1.介绍

开发环境:Hololens2 + unity2020.3 +vs2019+python3.8

最近一直在研究如何让hololens与电脑通信,查阅资料发现使用Socket可以实现通信。

让hololens与电脑通信,其实就是做到让unity和电脑通信,然后根据需求开发hololens端的UI即可。

项目流程大概是在unity中编写C#脚本捕获和发送图片,电脑端用python接收图片并保存。

在UI方面,设计两个Hololens Button,用来挂载两个方法,一个控制捕获照片,另一个控制将捕获的照片发送到python服务器端。后续工作是想用接收来的图片来做图像处理等。

下面是一张unity项目图

Python如何和unity3d通信 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场景。

Python如何和unity3d通信 python与unity交互_hololens_02

导入XR Plug-in Management,按照下图设置

Python如何和unity3d通信 python与unity交互_客户端_03


创建一个C#脚本文件,将上面客户端代码复制进去。代码中string serverIP可设为同一局域网下电脑的IP,如果在unity中调试可先设为127.0.0.1。

创建一个空节点命名为HololensCamera,来挂载刚刚创建的脚本。

接下来给脚本关联两个Button。

打开Toolbox,里面有很多Hololens按钮,我选择的是NearMenus下面的第一个

Python如何和unity3d通信 python与unity交互_经验分享_04


Python如何和unity3d通信 python与unity交互_unity_05


可以给按钮菜单栏重命名为Menu,在ButtonCollection节点下面有三个按钮,因为只用两个按钮,所以我删除了一个。可以给这两个按钮节点重命名为Capture个Send来方便区分。在按钮的Inspector里面可以设置图标,在IconAndText中可以设置显示文字。

Python如何和unity3d通信 python与unity交互_经验分享_06


将两个按钮拖拽到设置的空节点挂载的脚本的两个所需的Button引用上

Python如何和unity3d通信 python与unity交互_经验分享_07


这样以后unity项目就建立完成

3.2服务器端

在pycharm建立工程将python代码复制进去,导入所需的包。其中的ip也要设为自己电脑的ip。如果本机调试就可以先设为127.0.0.1 端口号和C#中同步。

4.调试

先运行服务器端代码,再运行unity启动Game模式,此时服务器端运行窗口会输出客户端连接的提示信息。在unity先点击拍摄按钮,在发送按钮。此时电脑端会受到图片,并将图片保存在了python项目同一文件夹下的received_image文件夹下。
在unity调试没有问题的话,再部署到hololens里面。
运行服务器,打开holo程序,点击捕捉按钮,然后发送,看一看图片有没有保存到文件夹里面。