文章目录
开发环境
Windows10专业版(开发者模式已打开)
Hololens2(开发者模式已打开,研究者模式已打开)
Visual Studio 2019
Unity2019.4.36.f1c1
Windows 10 SDK 10.0.18362.0
Python3.9(opencv-python)
1. 编译HoloLens2-Unity-ResearchModeStreamer
1.1 下载Github工程
HoloLens2-Unity-ResearchModeStreamer
该项目打开后,先阅读README.md文件,本实验是基于此文件操作的
1.2 编译HL2RmStreamUnityPlugin.dll
1)用VS打开下载好的项目里的HL2RmStreamUnityPlugin.sln文件
2)将项目属性设为Release和ARM64
3)注释工程项目中Eigen库
4)生成解决方案
编译若出现E1696错误提示,解决方法可参考文章后面的常见错误
输出是dll,输出位置在
Your Location\HoloLens2-Unity-ResearchModeStreamer\HL2RmStreamUnityPlugin\ARM64\Release\HL2RmStreamUnityPlugin\
2. 配置Unity项目
2.1 Unity的3D项目
可以新建项目,也可以使用下载好的项目里的UnityHL2RmStreamer项目
2.1.1 新建Unity的3D项目
新建项目后,将项目切换到Universal windows patform平台
新建项目可以参考我之前的博文:Holoens2开发环境配置及项目程序部署里的2.1新建项目
或者直接使用下载好的项目里的UnityHL2RmStreamer项目
2.1.2 使用UnityHL2RmStreamer项目
若直接使用下载好的项目里的UnityHL2RmStreamer项目
1)先删除原项目里Assets文件夹下的的Scripts和Plugins文件夹
Your Location\HoloLens2-Unity-ResearchModeStreamer-master\UnityHL2RmStreamer\Assets
2)用Unity Hub打开该项目,再重新创建证书
点击工具栏File → Build Settings→ Player Settings→ Player
2.2 新建文件路径
在unity项目里的Assets文件夹下新建两个分别命名为Scripts和ARM64的文件夹
路径分别为
Assets/Scripts
Assets/Plugins/WSAPlayer/ARM64
2.2 新建文件
2.2.1 HL2RmStreamUnityPlugin.dll
将前面生成解决方案编译好的文件HL2RmStreamUnityPlugin.dll拷贝到新建的ARM64文件夹下
2.2.2 StreamerHL2.cs
在Scripts文件夹中新建C#脚本文件,命名为:StreamerHL2.cs
StreamerHL2.cs内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Runtime.InteropServices;
public class StreamerHL2 : MonoBehaviour
{
[DllImport("HL2RmStreamUnityPlugin", EntryPoint = "Initialize", CallingConvention = CallingConvention.StdCall)]
public static extern void InitializeDll();
void Start(){InitializeDll();}
void Update(){}
}
2.2.3 挂载StreamerHL2.cs脚本
将StreamerHL2.cs文件拖到Main camera上实现挂载
3. 配置Unity的package文件中的兼容性
3.1 添加组件
点击工具栏File → Build Settings→ Player Settings→ Player → Capabilities
勾选InternetClient, InternetClientServer, PrivateNetworkClientServer, WebCam, SpatialPerception
3.2 编译unity工程
注意架构选择的是ARM64. 然后build到一个新文件夹
新文件夹最好是在此项目工程文件夹里
3.3 配置Unity的package文件中的兼容性
打开编译好的Unity工程文件夹中的“Package.appxmanifest”文件,按照官方文档修改3处内容(注意空格)
1)将如下字段添加到 Package 中
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
2)添加“rescap”字段
还是刚才的Package这一行中,找到"IgnorableNamespaces"的属性,添加“rescap”字段
3)在 Capabilityes添加如下字段
<rescap:Capability Name="perceptionSensorsExperimental" />
修改后的Package.appxmanifest文件如下,RMSTest可更换为自己的文件名
Camera_Stream:unity编译后的程序名(默认项目名)
DefaultCompany:unity项目默认公司名
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2" xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" xmlns:iot="http://schemas.microsoft.com/appx/manifest/iot/windows10" xmlns:mobile="http://schemas.microsoft.com/appx/manifest/mobile/windows10" IgnorableNamespaces="uap uap2 uap3 uap4 mp mobile iot rescap" xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10">
<Identity Name="Template3D" Publisher="CN=DefaultCompany" Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="edd907fa-2c4f-49b2-89a0-9fa371043c33" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
<Properties>
<DisplayName>Camera_Stream</DisplayName>
<PublisherDisplayName>DefaultCompany</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.10240.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="Template3D.App">
<uap:VisualElements DisplayName="Camera_Stream" Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\Square44x44Logo.png" Description="Template_3D" BackgroundColor="transparent">
<uap:DefaultTile ShortName="Camera_Stream" Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#FFFFFF" />
<uap:InitialRotationPreference>
<uap:Rotation Preference="landscape" />
<uap:Rotation Preference="landscapeFlipped" />
<uap:Rotation Preference="portrait" />
<uap:Rotation Preference="portraitFlipped" />
</uap:InitialRotationPreference>
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="perceptionSensorsExperimental" />
<Capability Name="internetClient" />
<Capability Name="internetClientServer" />
<Capability Name="privateNetworkClientServer" />
<uap2:Capability Name="spatialPerception" />
<DeviceCapability Name="webcam" />
</Capabilities>
</Package>
4. 项目部署
可以参考我之前的博文:Holoens2开发环境配置及项目程序部署里的 3.程序部署到hololens2设备(VS部署)
5. python获取数据流
5.1 hololens2_simpleclient.py
打开下载好的github工程中的hololens2_simpleclient.py文件
Your Location\HoloLens2-Unity-ResearchModeStreamer\py\hololens2_simpleclient.py
5.2 修改局域网
修改hololens2_simpleclient.py文件里的ip地址:HOST
这个是hololens2的IPv4 地址
5.3 查看结果
1)部署到hololens2里的程序已打开
2)hololens2与PC端处在同一个局域网下(hololens2可以连接PC端热点)
3)PC端的防火墙已关
4)运行修改完后的hololens2_simpleclient.py文件
运行时若出现** WinError 10060错误提示,解决方法可参考文章后面的常见错误**
5.3 将图像信息保存为图片
以下为修改后的hololens2_simpleclient.py文件,运行前同样要修改局域网
import os
import socket
import struct
import abc
import threading
import time
from datetime import datetime, timedelta
from collections import namedtuple, deque
from enum import Enum
import numpy as np
import cv2
# np.warnings.filterwarnings('ignore')
# 上行代码更改如下
import warnings
warnings.filterwarnings('ignore', category=np.VisibleDeprecationWarning)
# Definitions
# Protocol Header Format
# see https://docs.python.org/2/library/struct.html#format-characters
VIDEO_STREAM_HEADER_FORMAT = "@qIIII18f"
VIDEO_FRAME_STREAM_HEADER = namedtuple(
'SensorFrameStreamHeader',
'Timestamp ImageWidth ImageHeight PixelStride RowStride fx fy '
'PVtoWorldtransformM11 PVtoWorldtransformM12 PVtoWorldtransformM13 PVtoWorldtransformM14 '
'PVtoWorldtransformM21 PVtoWorldtransformM22 PVtoWorldtransformM23 PVtoWorldtransformM24 '
'PVtoWorldtransformM31 PVtoWorldtransformM32 PVtoWorldtransformM33 PVtoWorldtransformM34 '
'PVtoWorldtransformM41 PVtoWorldtransformM42 PVtoWorldtransformM43 PVtoWorldtransformM44 '
)
RM_STREAM_HEADER_FORMAT = "@qIIII16f"
RM_FRAME_STREAM_HEADER = namedtuple(
'SensorFrameStreamHeader',
'Timestamp ImageWidth ImageHeight PixelStride RowStride '
'rig2worldTransformM11 rig2worldTransformM12 rig2worldTransformM13 rig2worldTransformM14 '
'rig2worldTransformM21 rig2worldTransformM22 rig2worldTransformM23 rig2worldTransformM24 '
'rig2worldTransformM31 rig2worldTransformM32 rig2worldTransformM33 rig2worldTransformM34 '
'rig2worldTransformM41 rig2worldTransformM42 rig2worldTransformM43 rig2worldTransformM44 '
)
# Each port corresponds to a single stream type
VIDEO_STREAM_PORT = 23940
AHAT_STREAM_PORT = 23941
HOST = '192.168.137.34'
HundredsOfNsToMilliseconds = 1e-4
MillisecondsToSeconds = 1e-3
class SensorType(Enum):
VIDEO = 1
AHAT = 2
LONG_THROW_DEPTH = 3
LF_VLC = 4
RF_VLC = 5
class FrameReceiverThread(threading.Thread):
def __init__(self, host, port, header_format, header_data):
super(FrameReceiverThread, self).__init__()
self.header_size = struct.calcsize(header_format)
self.header_format = header_format
self.header_data = header_data
self.host = host
self.port = port
self.latest_frame = None
self.latest_header = None
self.socket = None
# ----------------------------------------添加锁对象---------------------------------------
self.lock = threading.Lock() # 新增这一行
def get_data_from_socket(self):
# read the header in chunks
reply = self.recvall(self.header_size)
if not reply:
print('ERROR: Failed to receive data from stream.')
return
data = struct.unpack(self.header_format, reply)
header = self.header_data(*data)
# read the image in chunks
image_size_bytes = header.ImageHeight * header.RowStride
image_data = self.recvall(image_size_bytes)
return header, image_data
def recvall(self, size):
msg = bytes()
while len(msg) < size:
part = self.socket.recv(size - len(msg))
if part == '':
break # the connection is closed
msg += part
return msg
def start_socket(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.host, self.port))
# send_message(self.socket, b'socket connected at ')
print('INFO: Socket connected to ' + self.host + ' on port ' + str(self.port))
def start_listen(self):
t = threading.Thread(target=self.listen)
t.start()
@abc.abstractmethod
def listen(self):
return
@abc.abstractmethod
def get_mat_from_header(self, header):
return
class VideoReceiverThread(FrameReceiverThread):
def __init__(self, host):
super().__init__(host, VIDEO_STREAM_PORT, VIDEO_STREAM_HEADER_FORMAT,
VIDEO_FRAME_STREAM_HEADER)
def listen(self):
while True:
# self.latest_header, image_data = self.get_data_from_socket()
# self.latest_frame = np.frombuffer(image_data, dtype=np.uint8).reshape((self.latest_header.ImageHeight,
# self.latest_header.ImageWidth,
# self.latest_header.PixelStride))
header, image_data = self.get_data_from_socket()
with self.lock: # 现在可以使用锁了
self.latest_header = header
self.latest_frame = np.frombuffer(image_data, dtype=np.uint8).reshape(
(header.ImageHeight, header.ImageWidth, header.PixelStride))
# def get_mat_from_header(self, header):
# pv_to_world_transform = np.array(header[7:24]).reshape((4, 4)).T
# return pv_to_world_transform
class AhatReceiverThread(FrameReceiverThread):
def __init__(self, host):
super().__init__(host,
AHAT_STREAM_PORT, RM_STREAM_HEADER_FORMAT, RM_FRAME_STREAM_HEADER)
def listen(self):
while True:
# self.latest_header, image_data = self.get_data_from_socket()
# self.latest_frame = np.frombuffer(image_data, dtype=np.uint16).reshape((self.latest_header.ImageHeight,
# self.latest_header.ImageWidth))
header, image_data = self.get_data_from_socket()
with self.lock: # 现在可以使用锁了
self.latest_header = header
self.latest_frame = np.frombuffer(image_data, dtype=np.uint16).reshape(
(header.ImageHeight, header.ImageWidth))
# def get_mat_from_header(self, header):
# rig_to_world_transform = np.array(header[5:22]).reshape((4, 4)).T
# return rig_to_world_transform
if __name__ == '__main__':
# 创建保存目录
os.makedirs("rgb", exist_ok=True)
os.makedirs("depth", exist_ok=True)
video_receiver = VideoReceiverThread(HOST)
video_receiver.start_socket()
ahat_receiver = AhatReceiverThread(HOST)
ahat_receiver.start_socket()
video_receiver.start_listen()
ahat_receiver.start_listen()
last_save_time = time.time()
# while True:
# if np.any(video_receiver.latest_frame) and np.any(ahat_receiver.latest_frame):
#
# cv2.imshow('Photo Video Camera Stream', video_receiver.latest_frame)
# if cv2.waitKey(1) & 0xFF == ord('q'):
# break
#
# cv2.imshow('Depth Camera Stream', ahat_receiver.latest_frame)
# if cv2.waitKey(1) & 0xFF == ord('q'):
# break
try:
while True:
# 使用线程锁确保获取完整的帧
with video_receiver.lock, ahat_receiver.lock:
if video_receiver.latest_frame is not None and ahat_receiver.latest_frame is not None:
# 显示图像
cv2.imshow('RGB Stream', video_receiver.latest_frame)
depth_colormap = cv2.applyColorMap(
cv2.convertScaleAbs(ahat_receiver.latest_frame, alpha=0.03),
cv2.COLORMAP_JET
)
cv2.imshow('Depth Stream', depth_colormap)
# 保存逻辑(每秒10次)
current_time = time.time()
if current_time - last_save_time >= 0.1:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
# 保存RGB(BGR格式直接保存)
rgb_path = f"rgb/{timestamp}_rgb.jpg"
cv2.imwrite(rgb_path, video_receiver.latest_frame)
# 保存深度原始数据(16位PNG)
depth_path = f"depth/{timestamp}_depth.png"
cv2.imwrite(depth_path, ahat_receiver.latest_frame)
print(f"Saved: {rgb_path}, {depth_path}")
last_save_time = current_time
# 退出控制
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
cv2.destroyAllWindows()
print("程序正常退出")
6. 解决实时数据流传输
本节参考文章Hololens2初入——解决HL真机到PC图像传输的实时性问题
6.1 思路一:降低hololens2往PC端发送数据的频率
HL2RmStreamUnityPlugin.cpp文件中:
1)在第60行左右找到以下代码,并按图示更改,将第一个传输参数改为2000000。这边更改的是彩色图像的传输频率
(这里的 2000000是200ms的意思,程序里面采用CPU时间单位,即百纳秒,1ms = 10^4百纳秒即5Hz)
若没有这三个参数
找到
co_await m_pVideoFrameProcessor->InitializeAsync(m_pVideoFrameStreamer)
改为
co_await m_pVideoFrameProcessor->InitializeAsync(m_pVideoFrameStreamer, 2000000);
2)找到134行按照下面图示更改,这边更改的是深度图像的传输频率,同样改为5Hz
6.2 思路二:增大wifi的通讯带宽,提升接收频率(换个好的网卡)
普通无线网卡 1-2Hz
5G频段,1200MB 的wifi6模块 USB接口 5Hz-10Hz
1200MB路由 +高速网线 5Hz左右
PCI-E 5G频段 3200MB 10-15Hz左右
6.3 思路三:压缩数据,减少传输字节
常见错误
E1696
E1696 无法打开源文件 “stdio.h”
解决方法:
更新一下SDK
1)打开Visual Studio Installer,点击修改
2)安装详细信息中自己系统对应的SDK,点击修改即可
WinError 10060
方法来源
解决方法:
1.先看研究模式有没有打开。
2.Unity buildsetting中的几个InternetClient, InternetClientServer, PrivateNetworkClientServer, WebCam, SpatialPerception确认勾选上
DllNotFoundException: HL2RmStreamUnityPlugin
DllNotFoundException: HL2RmStreamUnityPlugin StreamerHL2.Start () (at Assets/Scripts/StreamerHL2.cs:12)Failed to pause IContinuousRecognitionSession (hr = 0x80131509)
不需要在unity中运行工程,因为我们添加的是ARM64的动态链接库,在PC端运行的话会报加载不了的错误,此错误可以忽略,以下方法只是确保文件存在且导入进项目里
解决方法:
1)确保HL2RmStreamUnityPlugin.dll文件存在于项目的Assets/Plugins/WSAPlayer/ARM64文件夹中,HL2RmStreamUnityPlugin.dll文件没有问题
2)在StreamerHL2.cs中,检查调用DLL的代码,确保名称正确
确定脚本正确挂载
[DllImport("HL2RmStreamUnityPlugin", EntryPoint = "Initialize", CallingConvention = CallingConvention.StdCall)]
3)清理并重新导入项目
删除Library文件夹(位于项目根目录),然后重新打开Unity。Unity会重新生成所有缓存文件。
在Unity编辑器中,右键点击Assets文件夹并选择Reimport All,确保所有资源重新导入。
4)加载原场景
在原场景运行前,可先取消StreamerHL2脚本复选框,运行没问题后再勾选运行
原场景(.unity文件)路径:
Assets文件夹
或者Assets/Scenes文件夹
参考文章:
Hololens2 初入——获取彩色和深度图像数据流,并传递到程序中(不是网页浏览)
HoloLens2的彩色和深度数据流通过主机获取
主机端实时获取Hololens2的RGBD数据流