尝试打包WebGL平台遇到的坑

在将一个 Unity 2022.3 数字孪生项目(利用Unity3D_Robotics_ABB)从原 Win 平台迁移到 WebGL 平台的过程中,遇到了 RuntimeError: null function or function signature mismatchThe script 'xxxxx' could not be instantiated!digest auth failed 等等报错和故障。在对网上的解决方案探索和尝试的过程中,有小结如下

System.Threading 相关命名空间(C# 原生线程)不支持

这是着手切换WebGL平台之前就知道不行的一点,不过使用 UniTask 可以替换。

Unity3D_Robotics_ABB 作者在其实现中主要使用了原生的 Thread 来处理异步的逻辑,在 Unity 打 WebGL 包时并没有报错且能成功出包,然而,在运行 WebGL 的程序时(尤其做某些会拉起线程的操作时),会出现卡死、弹窗报错RuntimeError: null function or function signature mismatch 的问题。

若出现上述问题,应该考虑利用 Unity 托管的异步实现(UniTask、协程等),或 async/await 关键词处理对应需求。

System.Net 相关命名空间不支持(需要利用Unity提供的 UnityWebRequest(UWR) 来进行 HTTP 请求)

这是解决问题花费时间比较长的一点。

在解决完线程的问题后,运行 WebGL 程序,发现会出现 The script 'xxxxx' could not be instantiated! 等控制台警告,且此脚本功能失效。经过测试后,发现脚本中若引用了 System.Net 等 .NET networking classes,则此脚本不能被正确加载,见 WebGL Networking ,此时需要使用 UnityWebRequest 类进行 http 请求。

由于 UnityWebRequest 没有对 Authentication 的支持,需要手动处理一些问题:digest auth failed 。例如,Basic 验证方案可以自己加Header 并存入对应值。然而,对于 Digest 验证方案,手动处理比较麻烦。此时考虑调 JavaScript 的加 Digest 验证的方法并发送 HTTP 请求,或者由代理服务器添加 Digest 验证

技术水平有限,最终选择了由代理服务器添加 Digest 验证的方法,下面是对应的 Python 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from flask import Flask, request, Response
import requests
from requests.auth import HTTPDigestAuth

app = Flask(__name__)

# The URL of the API that requires Digest Authentication
TARGET_API_URL = "http://www.tsingloo.com"
# Replace 'your_username' and 'your_password' with your actual credentials
USERNAME = 'your_username'
PASSWORD = 'your_password'

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy(path):
# Construct the full URL to the target API
url = f"{TARGET_API_URL}/{path}"

# Log the requested URL
app.logger.info(f"I am requesting {url}")

# Get the original request method and data
method = request.method
data = request.get_data()

# Selectively copy headers, excluding those that can cause issues
excluded_headers = ['Host', 'Content-Length', 'Content-Type']
headers = {key: value for key, value in request.headers if key not in excluded_headers}

# If the request has a content type, include it in the headers
if 'Content-Type' in request.headers:
headers['Content-Type'] = request.headers['Content-Type']

# Inside your proxy function
response = requests.request(method, url, data=data, headers=headers)

# Make sure to copy the content-type header from the original response to the new response
content_type = response.headers.get('Content-Type')
response_headers = {'Content-Type': content_type} if content_type else {}

return Response(response.content, status=response.status_code, headers=response_headers)

@app.before_request
def log_request():
print(f"Received request: {request.url} - {request.method}")

if __name__ == '__main__':
app.run(debug=True)

短时间内利用 UWR 对 RWS 发送大量请求

为了追求项目的实时性,短时间内 Unity 利用 UWR 向机械臂(RWS)发送大量请求,前70个请求成功,而后的请求报 “503 Service Unavailable (Too many sessions 70/70)”错,且当双方断开连接时,仍需要一段时间“冷却”,而后才能请求正确收到回复,然后短时间内约70个 UWR 的 http 的请求后,重复上述 503 错误。使用C#提供的 HTTP 相关类时未见此问题。当503报错时,关闭代理服务器,重新插拔网线,都不能立刻释放此70个sessions。

值得注意的是,当 UWR 已经收到503报错时,再继续使用Postman测试相关接口,如(http://192.168.x.x/rw/rapid/tasks/T_ROB1/motion?resource=jointtarget)时,Postman有时可以正常收到数据。复盘此情况,推测是在排障中,由 Postman 已经发送了带 http头”Connection: Keep-Alive”并成功建立了正常的连接,早于 UWR 的请求,换言之,其 70 个session中已经维护了一个与postman的长连接。冷却后,使用 Postman 短时间内大量测试相关接口可以持续获得正常的数据,未见503报错。

冷却后,当使用Edge浏览器短时间内大量访问接口,前70个请求成功,而后的也会503报错,表现与 UWR 一样,且观察浏览器发出的 HTTP 请求,其已携带”Connection: Keep-Alive”,不同的是,浏览器发出的 HTTP 带有很长的“User-Agent”头,这一点没弄明白。

冷却后,当使用 Python 对接口短时间内大量发送简单的http请求(不挟带请求头”Connection: Keep-Alive”)时,前70个请求成功,而后的也会503报错,表现与 UWR 一样。

经检查,(编辑器 2022.3 版本默认的) UWR 发出的 HTTP 请求默认不携带标头 Connection: Keep-Alive,有讨论,见Connection: keep-alive in Unity Web Request?,且当使用 UnityWebRequest.SetRequestHeader 手动设置此标头时,会见警告且官方文档不推荐如此做。

最终,通过 UnityWebRequest.SetRequestHeader 手动设置标头”Connection: Keep-Alive”,解决了上述 503 报错。

几乎同一时刻对 RWS 发送多个请求

在将原线程替换为 UniTask 后,某个方法每帧将同时对 RWS 发送三个不同的 HTTP 请求,导致报 “Connection manager can’t add .. ” 错,改为一帧(约23ms)轮流发一个 HTTP 请求后解决。

HttpClient.PostAsyncUnityWebRequest.Post

使用PostAsync时,需要传入一个string和一个httpcontent,在使用 .Post 时,可以是两个string,也可以是一个string和一个 WWWForm,推荐使用后者,可以规避一些可能的编码问题。

反序列化时 dynamic 关键字引起的崩溃

项目中使用从 Unity Package Manager 中添加的 Newtonsoft.Json 来处理请求,原本从字符串中反序列化对象代码如下:

1
2
dynamic obj = JsonConvert.DeserializeObject(jsonStr);
var state = obj.name;

初次尝试打WebGL包时会报缺少某命名空间错,而后在项目设置中将 API 兼容性 级别改为 .Net Framewrok 后不再报错。
上述代码会导致WebGL运行时弹窗报错奔溃,推荐参考 json 的内容定义类型而后获取对应字段的值(JSON2CSharp)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Serializable]
public class People
{
public string name;
public int age;
public DateTime birthday;
public bool isMarried;
public float money;
}

public void DeserializeJson()
{
string jsonStr = "{\"name\" :\"张三\",\"age\":30,\"birthday\":\"1990-03-31T17:15:29\",\"isMarried\":true,\"money\":10.0}";

People zhangsan = JsonConvert.DeserializeObject<People>(jsonStr);
Debug.Log("zhangsan.name " + zhangsan.name + " birthday: " + zhangsan.birthday + " isMarried: " + zhangsan.isMarried);

//output: zhangsan.name 张三 birthday: 03/31/1990 17:15:29 isMarried: True
}

QFramework 随笔

参考资料:

  1. Unity游戏框架搭建决定版 QFramework的二次开发
  2. “您好 我是QFramework”
  3. QFramework v1.0 使用指南
  4. 基础 | 三层架构与MVC模式

Introduction

QFramework.cs 提供了 MVC、分层、CQRS、事件驱动、数据驱动等工具,除了这些工具,QFramework.cs 还提供了架构使用规范。

QFramework 内置模块如下:

  • 0.Framework:核心架构(包含一套系统设计架构)

  • 1.CoreKit: 核心工具库、插件管理

  • 2.ResKit:资源管理套件(快速开发)

  • 3.UIKit:UI 管理套件(支持自动绑定、代码生成)

  • 4.Audio:音频方案

    MVC

    MVC模式是软件工程中常见的一种软件架构模式,该模式把软件系统(项目)分为三个基本部分:**模型(Model)、视图(View)和控制器(Controller)**使用此模式有诸多优势,例如:简化后期对项目的修改、扩展等维护操作;使项目的某一部分变得可以重复利用;使项目的结构更加直观。

    1. **视图(View):**负责界面的显示,以及与用户的交互功能。实际在Unity中,这一部分往往指 UI 的呈现。

    2. 控制器(Controller):可以理解为一个分发器,用来决定对于视图发来的请求(命令),需要用哪一个模型来处理,以及处理完后需要跳回(通过事件更改)到哪一个视图。即用来连接视图和模型。

    3. **模型(Model):**模型持有所有的数据、状态和程序逻辑。模型接受视图数据(的命令),并返回最终的处理结果(,触发事件)。

      img

CQRS 命令和查询责任分离

一种将数据存储的读取操作和更新操作分离的模式。

Query 是一个可选的概念,如果游戏中数据的查询逻辑并不是很重的话,直接在 Controller 的表现逻辑里写就可以了,但是查询数据比较重,或者项目规模非常大的话,最好是用 Query 来承担查询的逻辑。

img

事件驱动

在事件驱动编程中,系统的流程是由外部事件(如用户输入或外部数据更改)驱动的。程序会对发生的事件做出反应。其核心思想是定义并使用事件处理器

用户输入 => 事件响应 => 代码运行 => 刷新页面状态

InputSystem 帮助Unity开发者将用户输入抽象为事件

数据驱动

操作UI(用户输入)=> 触发事件 => 响应处理 => 更新数据 => 更新UI(呈现)

BindableProperty<T> 提供了快速的绑定

Framework

系统设计架构,核心概念包括Architecture、Command、Event、Model、System

Architecture

可以将Architecture视为一个项目模块的管理器(System)的集合, 省去创建大量零散管理器单例的麻烦

使用注册的方式将当前项目所使用的 模块 系统和 工具 添加进内部的IOC容器中,方便管理。

Controller

赋予 MonoBehaviour 脚本对象访问架构的能力

Model

同类的公共数据

架构规范与推荐用法

QFramework 架构提供了四个层级:

  • 表现层:IController
  • 系统层:ISystem
  • 数据层:IModel
  • 工具层:IUtility

通用规则

  • IController 更改 ISystem、IModel 的状态必须用Command
  • ISystem、IModel 状态发生变更后通知 IController 必须用事件或BindableProperty
  • IController可以获取ISystem、IModel对象来进行数据查询
  • ICommand、IQuery 不能有状态,
  • 上层可以直接获取下层,下层不能获取上层对象
  • 下层向上层通信用事件
  • 上层向下层通信用方法调用(只是做查询,状态变更用 Command),IController 的交互逻辑为特别情况,只能用 Command

表现层

ViewController 层。

IController接口,负责接收输入和状态变化时的表现,一般情况下,MonoBehaviour 均为表现层

  • 可以获取 System、Model
  • 可以发送 Command、Query
  • 可以监听 Event

系统层

System层

ISystem接口,帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等

  • 可以获取 System、Model

  • 可以监听Event

  • 可以发送Event

数据层

IModel接口,负责数据的定义、数据的增删查改方法的提供

  • 可以获取 Utility
  • 可以发送 Event

工具层

Utility层

IUtility接口,负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等。啥都干不了,可以集成第三方库,或者封装API

常用技巧

TypeEventSystem

独立的事件系统,可以理解为群发和广播,创建结构体传递事件内容

1
public struct PlayerDieEvent { }

一些简单的事件可以通过 .Register

WildPerception

WildPerception 是一个利用 Unity Perception Package 来生成大规模多视角视频数据集的工具。
其允许用户导入自己的 Humanoid 人物模型,或者利用 SyntheticHumans Package 以合成人物模型,从而在自定义的场景中模拟行人。配合 MultiviewX_Perception 可以得到符合 Wildtrack 格式的数据集。

注意:

  1. 原 MultiviewX_FYP 现更名为 MultiviewX_Perception
  2. CalibrateTool 已集成到了此处,将不再独立导出
  3. 开发使用的 Editor 版本为 2022.3.3f1,不保证之前版本(尤其是2022.2之前)的表现。 Unity Perception Package 要求一定版本的 HDRP 。

Support For SyntheticHumans

  1. 添加 WildPerception、 SyntheticHumans Package 到您的项目中,推荐导入 SyntheticHumans 官方提供的 Samples,具体过程请参考:Install Packages and Set Things Up

  2. 为 TsingLoo.WildPerception.asmdef 添加 AssemblyDefinitionReferences,选择 SyntheticHumans 提供的 Unity.CV.SyntheticHumans.Runtime,而后在页面底部右下角点击 Apply,保存。

    选择 SyntheticHumans 提供的 Unity.CV.SyntheticHumans.Runtime

    在页面底部右下角点击 Apply,保存

  3. 找到 RuntimeModelProvider.cs 脚本,更改 false 为 true

    更改 false 为 true

  4. 将 RuntimeModelProvider 组件分配给场景

    将 RuntimeModelProvider 组件分配给场景

  5. 移除其他的ModelProvider

    移除其他的ModelProvider

  6. 添加 HumanGenerationConfig,Config 的具体配置请参考此文档下半部分:Generate Your First Humans

添加 HumanGenerationConfig

  1. 运行

Import

有两种方法可以将 WildPerception 包添加到您的项目中。

注意:

  1. 由于需要安装相关依赖,应该在联网环境中导入此包

[Method 1] Add package from git URL

注意:

  1. 这需要您的设备有 git 环境,可以去这里安装:git
  2. 由于目前 Package Manager 的限制,若如此做不可打开示例场景 SampleScene_WildPerception,但不影响其他功能。

Unity Editor 中打开 Window -> Package Manager

Add package from git URL

复制本项目地址,填写并添加

复制本项目地址

填写并添加

大约三到五分钟后,添加完成,Unity 开始导入新包。

[Method 2] Add package from disk

通过 git clone 或者直接从 github 上下载ZIP文件,或者从此处[Download WildPerception] 下载包。

直接从 github 上下载ZIP文件

将此ZIP文件解压,放到非项目Assets文件夹中(在项目文件夹外亦可)

放到非项目Assets文件夹中

Unity Editor 中打开 Window -> Package Manager

Add package from disk

将package.json选中并确定。

将package.json选中

大约两分钟后,添加完成,Unity 开始导入新包。

Setup

您可以很快赋予您的场景生成序列帧的能力,只需要简单的几步配置。

若您使用的是方法2来添加此包,不妨打开场景 Packages -> WildPerception -> Sample -> SampleScene_WildPerception。 这个场景中比较简洁,如下图所示,有预制体SceneController 和一些GameObject

需要用到的预制体及生成的GameObject

若您想使用自己搭建的场景,请为这个场景添加预制体 SceneController

为这个场景添加预制体 SceneController

SceneController

SceneController 集成了所有的配置与功能。

场景导入 SceneController 后,可以打开其Inspector面板,请按照您的情况配置。

Main Controller

MainController 是 SceneController上挂载的一个组件

点击 Init Scene,场景中会自动生成所需的GameObject,请在场景中将这些 GameObject 置于所需的位置,随后点击 Assign Transfrom

请设置 MultiviewX_Perception 项目的绝对路径

若使用 LocalFilePedestrianModelProvider, 请设置 Mode_PATH 的绝对路径,此路径务必在一个路径包含”Resources/Models”的文件夹下且此文件夹中有且仅有人物模型的预制体(.prefab),而非.fbx等模型文件。若您的项目中还没有人物模型,可以使用示例模型,其在 com.tsingloo.wildperception-main\Resources\Models 下,请使用其绝对路径。

设置相关路径

对于人物模型,仅仅要求其具有 Humanoid 骨骼,并带有 Animator组件,其 Runtime Animator Controller 可以为空,若为空,将会在其生成时使用您在 People Manager 中配置的默认Runtime Animator Controller。

仅仅要求其具有 Humanoid 骨骼,并带有 Animator 组件

注意:

  1. 请尽量保证GridOrigin_OpenCV的纵坐标(Y)与Center_HumanSpawn_CameraLookAt的纵坐标(Y)相同,否则可能会出现标定不准确的情况,这个问题可能会在后续工作中修复。
  2. 确保场景中供人物模型行走的平面携带有 NavMeshSurface 组件,并已完成烘焙,若您未在 Add Component 中找到这个组件,请前往安装 Package Manager -> Packages: Unity Registry -> AI Navigation,此处使用的版本是 1.1.1

供人物模型行走的平面携带有 NavMeshSurface 组件

Camera Manager

Camera Manager 管理并控制着相机相关的内容,您可以在这里配置将在场景运行多少帧后开始导出序列帧(Begin Frame Count)、相机的位置类型(自动生成 Ellipse_Auto 或者手动摆放 By Hand)、相机的自动生成参数 Ellipse_Auto Settings

Camera Place Type

若您希望程序自动生成相机,请使用Auto

若您希望手动放置相机,请使用 By Hand
并从菜单中添加相机,为这个场景添加相机预制体 Camera_Perception,并将预制体放在HandPlacedCameraParent下

为这个场景添加相机预制体

将相机预制体放在HandPlacedCameraParent下

Ellipse_Auto Settings

此处您可以配置相机的自动生成参数。

参数名 备注
Level 有几层相机
Nums Per Level 每层多少个相机
Height First Level 第一层离地(LookAt) 多少垂直高度(已经换算为了OpenCV下长度)
H Per Level 若有多层,每层层高(已经换算为了OpenCV下长度)
Major Axis 椭圆主轴长度 (已经换算为了OpenCV下长度)
Minor Axis 椭圆副轴长度 (已经换算为了OpenCV下长度)
Camera Prefab 将会被自动生成的相机的预制体

Pedestrians Manager

此处您可以配置人物生成的相关参数。

参数名 备注
Default Animator 人物模型使用的默认动画控制器
Add_human_count 每次敲击空格将会新生成几个人物模型
Preset_humans 场景初始化后生成多少个模型
Largest,Smallest,X,Y 规定初始化生成模型的区域(绿色矩形辅助线)以及行人的活动范围
Outter Bound Radius 人物模型回收边界,请设置大一些,务必使此边界不与 Grid 相交,否则可能出现Editor 卡死,这个问题会在后续工作中修复

AbstractPedestrianModelProvider

实现此类以能为 Pedestrians Manager 提供行人模型(将其拖拽到 Main Controller 的 Pedestrian Model Provider 属性槽中,默认为 LocalFilePedestrianModelProvider)

CalibrateTool

用于相机的标定,为 MultiviewX_Perception 提供数据,请见:CalibrateTool

Notes of the calibration of MultivewX_Perception(CalibrateTool)

WHAT IS NEW!!

支持利用数据集文件夹中 calibrations 中的标定数据与 datasetparameters.py 中的 NUM_CAM MAP_HEIGHT MAP_WIDTH OverlapUnitConvert OverlapGridOffset 参数给出其相机姿态和视野范围。

例如,当我们希望为 Wildtrack 数据集产生 Overlap view 时:

在命令行中输入 -view D:\Wildtrack ,注意需保证其calibrations文件夹下有extrinsicintrinsic文件夹(与Wildtrack格式一致)。

运行可得下列结果。

Camera 6

左图为本工具对 Wildtrack 的结果,右图(有一定变形)为参考

注意:请修改上述的五个参数以符合实际情况,MAP 使用的单位应当与其 calibration 使用的单位相同。Wildtrack 数据集工具声明其网格起点与世界原点并非同处,网格起点为(-300,-90,0)cm,似乎有误。此处使用的是网格起点为(-300,-900,0)cm

Args

MultiviewX_Perception可以接受命令行参数,从而用户可以快速高效地产生数据集。当未接收到相关参数时候,不会启用相关功能。

-a :Annotate and show the bbox on the first frame of each camera or not.

-s :Save the bbox on the first frame of each camera or not.

-k :Keep the remains of Perception dataset or not.

-f :Force calibrate and generate POM, regardless of perception.

-p n: Provide preview for the front n frames. ex. -p 5 will provide 5 frames to preview

-v :Generate Overlap view for the dataset

-view path :Generate Overlap view for the specified dataset, there should be folder calibrations in the given path. ex. -view D:\Wildtrack

例如,当只想借助CalibrateTool进行标定时,可以输入-f,程序会跳过处理percetion数据步骤,也不会进行后续标注的环节。

1
python run_all.py -f

Keep in Mind

CalibrateTool 现在是 WildPerception 的标定部分

Github 项目地址:MultiviewX_WildPerception

欢迎下载示例文件:sample.zip

对于场景:

  1. Unity长度(米)➗ Scaling = OpenCV长度(米)

  2. (Unity点坐标 - Unity中GridOrigin的Unity坐标)➗Scaling , 再交换Unity坐标中的y,z分量,可以得到OpenCV下点坐标。

  3. GridOrigin所在位置是OpenCV下的坐标原点

  4. 棋盘会在黄色辅助正方体内随机生成,辅助正方体的边长等于两倍的tRandomTransform,其中心是chessboardGenerateCenter

  5. 场景中全体markpoint_3dchessboardGenerateCenter的坐标加上一些预设的偏移得到。换言之,场景其实只有一份markpoints_3d,其中心为 chessboardGenerateCenter,均匀分布在水平面上。

    注意:Grid辅助线上标注的和辅助点markpoint_3d上标注的值已经是OpenCV下此点的坐标。

    如下图:

    image-20230322202441746

对于每个相机:

  1. 既然整个场景公用一份markpoints_3d,为什么每个相机下都有markpoints_3d.txt这么个文件呢?

    因为不是全体markpoint_3d都在此相机的视野范围内,需要针对每个相机进行剔除对应点。每个相机的 markpoints_3d.txtmarkpoints_2d.txt 共同得出了其外参。

  2. 务必保证Game View下的分辨率与CalibrateTool配置的分辨率相同,否则会直接退出运行并报错。

  3. tRandomTransform的选择推荐是,使得黄色辅助正方体大部分在所有相机的视野中。

Introduction

CalibrateTool是一个在Unity3D中为一个或多个相机,产生多个虚拟的不同角度朝向的棋盘格数据且给出待标定相机对应内外参的工具。其生成的虚拟棋盘数据等效于利用OpenCV中cv.findChessboardCorners所产生的结果。同时,CalibrateTool 可以完成一些运行 MultiviewX_Perception 所需要的设置,诸如设置地图大小、地图格点起始位置等。具体使用在 [Work with MultiviewX_Perception](# Work with MultiviewX_Perception) 标题下。

下列图片为标注环节的效果演示,此环节不在CalibrateTool能力范围内,是MultviewX_Perception的后续环节。此处贴上标注的图仅仅用来说明CalibrateTool的缩放、OpenCV坐标系的设置、标定是合理有效的。

Grid地图

bbox\_cam9

bbox\_cam8

Setup

CalibrateTool.unitypackage 是对应的Unity资产,其中包含了一个带有CalibrateTool组件的预制体和组件对应的代码。(面板可能因为版本不同略有出入,推荐总是使用最新的一个版本)

  1. unitypackage包导入完成后,我们可以将 CalibrateTool 拖入到需要标定的场景中:

    检视面板

  2. 根据自己项目的情况进行配置,点击加号➕,产生空槽,将场景中需要标定的相机拖入,一个或者多个均可,同时,此字段是公开的,可以利用脚本进行赋值:

    点击➕,拖入待标定相机

    利用脚本进行赋值

  3. 调整相机分辨率,点击Game,选择一个具体的分辨率,此处以1920*1080为例

    选择一个具体的分辨率

  4. 传入一个Transform,chessboardGenerateCenter,用来指示虚拟棋盘的产生位置,同时也规定了标定参照的水平面,这个 Transform 的位置最好能在所需标定的相机的屏幕中央附近(此处为了演示此位置,创建了一个cube,实际使用中只需要创建一个空物体,传入Transform即可,不必考虑其旋转,将被统一清零):

    位置最好能在所需标定的相机的屏幕中央附近

    传入一个Transform

  5. 给定目标文件夹,CalibrateTool会在此文件夹下产生一个 calib 文件夹用来保存数据。一般会填入MultiviewX所在文件夹

    给定目标文件夹

  6. 传入一个Transform,Grid Origin用来指示Grid格点的原点,同时也是OpenCV坐标系(右手坐标系)的原点,为了方便计算,应当将此点设置在标定参照的水平面上(其Unity坐标的Y值应该与chessboardGenerateCenter的Y值相同,我没有测试过不相同会如何)。

    Grid Origin

    正确配置后,Scene场景中会产生辅助线。此图中,蓝色箭头指示右手坐标系下Y轴的正反向,红色箭头指示右手坐标系下X轴的正方向

    指示Grid格点的原点

  7. 可以通过调节MAP_HEIGHTMAP_WIDTH 来调节格点图的大小,MultivewX只会标注脚底在格点图中的人。默认值16 与 25 是一个合理的值,一般不需要额外改动。

    例如,当MAP_HEIGHT = 16 ,MAP_WIDTH = 8 时,标注如图:

    MAP\_WIDTH = 8

    bbox\_cam7

    MAP_EXPAND可以理解成每个边长被额外划分多少份(小刻度),改动此项不会改变地图的大小。

    例如,当MAP_EXPAND = 40 时:

    MAP\_EXPAND = 40

  8. 缩放(Scaling)。很多时候,场景素材的地图尺寸和人物模型尺寸是不一样的,往往会存在人物模型与场景的不协调。 下图展示了这种差异,人物模型看起来很小,成年人看起来身高和儿童一样。

    地图尺寸和人物模型尺寸这就导致用户难以一站式完成CalibrateTool的使用,需要自己手动再去调整人物模型或者场景素材,而往往这种调整还牵扯到matchings(MultiviewX接受的一种输入)的坐标变换,每个人实现matchings的方法都不一样,这里提个醒,坐标变换的顺序必须是: 缩放->旋转->平移

    通过调整Scaling参数,CalibrateTool的辅助线与辅助模型(立方体)可以帮助用户很直观的找到一个合理的缩放值。辅助线的每格的边长为右手坐标系下1米。辅助模型的长宽为MAN_RADIUS*2,高为MAN_HEIGHT

    给与生成辅助线所需引用

    辅助线与模型

  9. 一般情况下,其余参数不需要额外设置。如果Python端报错,可以尝试调大Update Chessboard Interval参数,增加IO读写的时间。

  10. 运行。

Calibrate

拿到数据后,我们就可以进行标定了,应该会有如下结构:calib文件夹下有 C1 - Cn 子文件夹,每个子文件夹中,有得到的棋盘数据。

image-20230301221916504

运行calibrateCameraByChessboard.py,内外参数分别保存在calibration/intrinsiccalibration/extrinsic中。所输出的外参数(对于每一个虚拟的棋盘,都有一个外参负责对应的变换)为对第一个虚拟棋盘的变换,且生成的第一个虚拟棋盘总是与给定的Chessboard Generate Center同面

虚拟棋盘始终与给定的Chessboard Generate Center同面

一个疑惑 这种方法中,Python收到的棋盘格的世界坐标是给定的,如上图所示。(0,0,0)总是在左下角 往往在一些相对Chessboard Generate Center物体对称的物体,会有一个很相近的tvec。 因为在单个相机时,相对于该相机描述时,物体总是需要做同样的位移变换。但是在多个相机时,这种描述,依旧是相对单个接受标定的相机。

为了解决上述的疑问,尝试采用了cv2.solvePnP,和一组在待标定(水)平面上的静态的点来得到R与T,目前不支持相对斜面标定外参。

一组在待标定平面上的静态的点

在引入Grid格点的原点的概念时,遇到了一些问题。原思路是直接变换MarkPoints,使得solvePnP ”认识“目标坐标系,但是,经过实验得知,变换MarkPoints时候,应当保证最终被solvePnP获取的(OpenCV中)世界坐标的x,y值,保证其正方向与原 Unity 中x,z正方向保持一致或全部相反。否则会导致后续POM生成失败。猜测是左右手坐标系变换后OpenCV下Y正方向的问题。

Validate

通过参考Unity3d和OpenCV的相机模型左右手坐标系下三维位姿(旋转、平移)的转换旋转向量和旋转矩阵的互相转换 python cv2.Rodrigues()得知,默认的 Unity3D 相机组件是一个理想的针孔相机,其内外参可以通过调用Unity3D给予的相关参数计算得出。

GetNativeCalibrationByMath(),给出了这种方法,其结果基于Unity场景下的世界坐标系。经过比较这两个方法获得的值,误差很小,可以认为CalibrateTool是可以合理利用的。

提供了 vali.py,其利用单应性检验标定结果。

Preview

cam1\_frames

支持动图预览

  • 例如,在命令行中输入 python run_all.py -p 15 ,即可为前15帧生成带标注框的动图预览。

Work with MultiviewX_Perception

请 clone 一份 MultiviewX_Perception,可以参考Notes of MultiviewX_Perception进行后续工作。其将原calibdateCamera.py替换为了calibrateByChessboard.py,添加了一些动态容量的数组以适应不同的摄像头数,支持Scaling缩放,并且datasetParameters将由 CalibrateTool 根据Unity中Inspector面板处的参数自动生成,等等。

欢迎下载示例文件sample.zip,将其子文件夹calibperceptionmatchings,子文件datasetParameters.py拖入到 MultiviewX_Perception 文件夹下。

黄色字体即拖入的文件

运行run_all.py,参考Notes of MultiviewX_Perception,其给出了一些常用的命令行参数

1
python run_all.py 

当用户仅仅需要标定与生成POM时候(仅仅提供calibdatasetParameters.py),可以输入参数-f:

1
python run_all.py -f 

如果遇到WinError 32 报错,请检查是否有相关程序正在使用.pom,在PyCharm中,可能不小心打开了.pom的预览窗口,请关闭。

image-20230325105913670

示例 datasetParameters.py (for Wildtrack):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GRID_ORIGIN = [-14.91,-1.51,-5.43]
NUM_CAM = 7
CHESSBOARD_COUNT = 50
MAP_WIDTH = 12
MAP_HEIGHT = 36
MAP_EXPAND = 40
IMAGE_WIDTH = 1280
IMAGE_HEIGHT = 720
MAN_HEIGHT = 1.8
MAN_RADIUS = 0.16
RJUST_WIDTH = 4
Scaling = 1
NUM_FRAMES = 0
DATASET_NAME = ''

# If you are using perception package: this should NOT be 'perception', output path of perception instead
PERCEPTION_PATH = 'D:/Test/WildPerception'

# The following is for -view configure only:

# Define how to convert your unit length to meter, if you are using cm, then 0.01
OverlapUnitConvert = 0.01
# Define how to translate cams to make the world origin and the grid origin is the same
OverlapGridOffset = (3., 9., 0.)