Releases: Jintian-JTST/SecondTouchReality
v1.1 SecondTouchReality
This version combines AI and Unity.
Full Changelog: SecondTouchReality...Unity
SecondTouchReality
SecondTouchReality 项目说明
SecondTouchReality 是由 OneTouchReality 演化而来的。
SecondTouchReality 是一个「虚拟现实手势控制教学物体系统」的衍生版本,重点在于「用自然语言生成物体 + 用捏合手势抓取物体 + 用单通道舵机反馈捏合状态」,并且把整体架构设计得更模块化、易扩展。SecondTouchReality 是一个小型但比较完整的“端到端”系统,延续了这条链路,但做到了「抽骨架」和「轻量化」:
- Python 端:手部追踪 + 深度估计 + 文本分类模型
- Unity 端:3D 手骨重建 + 抓取交互 + 视角控制
- 硬件端:Arduino / 舵机 / 手套(预留接口)
串成一条流水线:
摄像头 → Python 理解你的手 + 理解一句话 → Unity 生成 3D 场景并可交互 → 驱动硬件反馈
目前仓库处于原型阶段,但完整体现了“感知 → 语义 → 交互 → 硬件”的闭环。
1. 系统整体结构
从最终使用者视角,这套系统做了三件事:
-
看懂手
- Python 用 MediaPipe Hands 检测 21 个手部关键点。
- 计算掌宽、掌长、卷曲度 curl、侧向程度 side、掌心/手背朝向。
- 用一次标定把掌宽/掌长换算成掌根到摄像头的真实距离(米),并做滤波。
- 把 wrist 的 3D 位置、20 条骨骼方向向量和
pinch(捏合)状态打包成 JSON,用 UDP 发给 Unity。
-
看懂话
- 在 Unity 中弹出对话框,输入一句英文描述(如:"a small red apple")。
- Python 端小型文本模型用
HashingVectorizer + SGDClassifier做多分类,输出一个离散 label(比如 "101")。 - TCP 返回
"101"这种格式给 Unity。
-
摸东西
- Unity 用
HandFromVectors根据 UDP JSON 重建 3D 手部骨骼,用小球和线段画出来。 PinchGrabBall让你捏住场景里的物体,跟着手移动。HandOrbitCamera让你在没有抓任何东西的时候,通过 pinch + 移动手 来旋转/缩放视角。ModelLibrary/RuntimeModelLoader根据 label 加载对应的 3D 模型(预制体或 GLB 文件)。
- Unity 用
-
目录树
SecondTouchReality/
├── README.md # 英文/混合版说明(含总体设计)
├── README_CHN.md # 纯中文说明(含总体设计)
├── requirements.txt # Python 依赖
├── text_model.pkl # 训练好的文本分类模型
├── main.py # combined_server,一键启动整条链路
├── .gitignore
├── unecessary/ # 一些旧脚本或不再使用的资源
├── tools/
│ ├── hand_easy.py # 手部距离估计 + 标定逻辑
│ ├── hand_udp.py # 多手 + 骨骼向量 + pinch → UDP JSON
│ ├── arduino_udp_receive.py # 仅 UDP → Arduino 的桥接脚本
│ ├── text_infer_server.py # 文本模型推理 TCP 服务
│ ├── run_model.py # 加载 text_model.pkl,提供命令行推理
│ └── __pycache__/ # Python 缓存
├── test/
│ ├── collect_data.py # 采集「描述文本 + 标签」数据
│ ├── clean_dataset.py # 清洗 JSONL 数据集
│ ├── train_model.py # 训练文本分类模型并输出 text_model.pkl
│ ├── text_object_dataset.jsonl
│ ├── cleaned_text_object_dataset.jsonl
│ └── object_models_csv.csv # 文本标签 ↔ 模型 ID 映射表
├── Game/ # Unity demo 工程(可直接打开)
│ ├── SampleScene.unity # 主场景
│ ├── models/ # 若干 glb 模型(苹果、香蕉、碗等)
│ └── Scripts/ # 主要 C# 脚本
│ ├── HandFromVectors.cs
│ ├── HandOrbitCamera.cs
│ ├── PinchGrabBall.cs
│ ├── ModelLibrary.cs
│ ├── RuntimeModelLoader.cs
│ └── TextQueryClient_TMP.cs
└── ...
-
根目录
main.py:推荐的入口脚本。打开摄像头、启动 UDP → Unity 的手部数据流,同时挂载on_payload回调,把 pinch 状态映射到串口命令,再顺便启动文本推理服务器。requirements.txt:列出所有 Python 依赖包,通常用pip install -r requirements.txt一次装完。text_model.pkl:由test/train_model.py训练得到,用于把自然语言描述映射到物体标签。
-
tools/– 运行时工具层-
hand_easy.py:封装了距离标定与滤波逻辑,供hand_udp.py等脚本调用。 -
hand_udp.py:- 调用 MediaPipe,支持多只手;
- 输出掌根深度、骨骼方向、pinch 状态;
- 通过 UDP 把 JSON 包发往 Unity(默认
127.0.0.1:5065); - 允许外部设置
on_payload回调。
-
text_infer_server.py:启动一个基于socketserver的多线程 TCP 服务,使用run_model.py里的infer_once在内存中调用文本模型。 -
arduino_udp_receive.py:简化版桥接程序,从 UDP 收到 hand JSON 后,只关心hands[].pinch,检测状态变化并向 Arduino 串口发送'0'/'1'。
-
-
test/– 数据与模型实验区collect_data.py:交互式命令行工具,用来快速收集训练数据。clean_dataset.py:从原始数据中过滤掉含中文的样本,只保留干净的英文文本和标签,输出到cleaned_text_object_dataset.jsonl。train_model.py:对清洗后的数据训练模型,并将结果保存为text_model.pkl供主程序加载。
-
Game/– Unity demoHandFromVectors.cs:UDP 客户端,负责解析 Python 发送的 JSON,重建关节位置,并用小球和线段可视化。PinchGrabBall.cs:把任何 3D 物体变成“可捏”的物体,处理抓取、跟随、释放和平滑移动等逻辑。HandOrbitCamera.cs:用 pinch 控制视角旋转与缩放。ModelLibrary.cs:维护一个名字 → GameObject 的字典,提供ShowModelByLabel(label),供文本推理结果直接调用。TextQueryClient_TMP.cs:基于 TextMeshPro 的文本输入客户端,负责和 Python 文本服务器通信。RuntimeModelLoader.cs:支持运行时从StreamingAssets/models中按编号加载 glb 模型,可用于扩展模型库。
2. 技术栈
2.1 Python 端
- Python 3.x
- OpenCV (
cv2) – 摄像头采集 + HUD 绘制 - MediaPipe Hands – 手部关键点检测
- NumPy – 各种向量运算、统计量(中位数、EMA 等)
- scikit-learn – 文本特征(
HashingVectorizer)+ 线性分类器(SGDClassifier) - joblib – 模型与标签编码器序列化 (
text_model.pkl) - socket / socketserver – UDP + TCP 通信
- pyserial – 串口连接 Arduino
主要 Python 文件:
hand_udp.py– 主手部追踪 + UDP 推流hand_easy.py– 深度估计算法 demo / 调试collect_data.py/clean_dataset.py/train_model.py/run_model.py– 文本模型数据与训练工具链text_infer_server.py– 文本推理 TCP 服务main.py– 把手部追踪 + 文本服务 + 串口桥接综合到一个进程里arduino_udp_receive.py– 备选方案:UDP → 串口桥接
2.2 Unity / C# 端
- Unity 202x
- C# 脚本:
HandFromVectors.cs– UDP 收包 + 手骨重建 + GUI 调参PinchGrabBall.cs– 物体捏取逻辑HandOrbitCamera.cs– 摄像机绕场景目标旋转和缩放ModelLibrary.cs– 将子物体/预制体组成一个“模型字典”RuntimeModelLoader.cs– 用 GLTFast 动态加载.glbTextQueryClient.cs(类名TextQueryClient_TMP)– Unity 端的文本 TCP 客户端 + UI
- TextMeshPro – 输入框与文字显示
- GLTFast – 运行时加载 .glb / .gltf 模型
2.3 硬件端
- Arduino(Uno / Nano 等)
- 简单舵机若干(演示用)
- 串口协议极简:每次发送一个 ASCII 字符,比如
'0'/'1'。
3. 运行流程
一个典型的使用流程(对应你现在的操作)是:
- 先打开 Python 端(
main.py) - 在 Python 弹出来的摄像头窗口中按
c做标定,r重置,q退出。 - 再打开 Unity 工程,进入包含
Skin场景(或你自己的 demo 场景)。 - 在 Unity 里点击 Play:
- 看到 3D 手骨跟着你的真实手在动;
- pinch 时能旋转视角或抓取物体;
- 在对话框里输入一句话,自动生成对应的 3D 模型到你面前。
下面拆细一点。
3.1 配置 Python 环境
-
在项目根目录创建虚拟环境(建议):
python -m venv .venv .venv\Scripts\activate
-
安装依赖:
pip install -r requirements.txt
-
确认摄像头能被 OpenCV/MediaPipe 访问。
3.2 启动综合服务器 main.py
在项目根目录运行:
python main.py此时将有:
-
一个摄像头预览窗口,带 HUD 提示(FPS、标定状态等)。
-
后台开着:
- UDP 手部数据服务器(发给 Unity)。
- 文本 TCP 服务(监听
127.0.0.1:9009)。 - 串口(如果连接 Arduino 的话)。
在摄像头窗口中操作:
-
按
c:- 保持手掌张开,正对摄像头不动,它会采样 50 帧左右。
- 终端里会让你输入“此时掌根到摄像头的真实距离(米)”,比如
0.45。 - 程序根据采样到的掌宽/掌长中位数计算
k_w/k_l,以后就能大致估出 Z。
-
按
r:重置标定状态。 -
按
q:退出 Python 端。
标定完成后,HUD 上会从 “Calib: NOT SET” 变成类似 “Calib: OK”。
3.3 打开 Unity 场景
-
在 Unity 里打开本工程,对应的 demo 场景通常类似
Skin.unity:
场景中应该有:-
一个物体挂着
HandFromVectors:listenPort = 5065(要跟 Python 对应)。- 指定
targetCamera(一般是主摄像机)。
-
主摄像机上挂
HandOrbitCamera。 -
某个节点挂了
ModelLibrary,其子物体是各种模型模板(名字一般用 label 做文件名)。 -
一个 UI Canvas 里挂了
TextQueryClient_TMP,绑定 TMP 输入框和按钮。
-
-
点击 Play:
-
摄像机画面里会出现 3D 手骨(小球 + 线段)。
-
当手做 pinch(拇指+食指捏合)时:
- 如果附近有
PinchGrabBall物体,就会被抓住跟着手走; - 如果没抓到物体,
HandOrbitCamera会把 pinch 解释为“控制摄像机”——移动手改变视角。
- 如果附近有
-
-
在 UI 上点击按钮弹出对话框,输入一行英文描述,例如:
a green apple并点击确认按钮:
- Unity 把这串文本发到
127.0.0.1:9009。 - Python 调用文本模型,返回
102|0.93类似的结果。 TextQueryClient_TMP解析出label = "102",调用ModelLibrary.ShowModelByLabel("102")。ModelLibrary/RuntimeModelLoader在镜头前生成一个对应模型,并自动挂上PinchGrabBall让你捏住它。
- Unity 把这串文本发到
3.4 连接 Arduino
-
在 Arduino 中烧入一个简单的串口控制程序,例如:
Serial.begin(9600);if (Serial.available()) char c = Serial.read();- 若
c == '1'→ 舵机转到 45°,c == '0'→ 回到 0°。
-
在
main.py或arduino_udp_receive.py中,把COM9换成实际串口号。 -
运行 Python:
- 手追踪正常时,
on_payload或arduino_udp_receive会根据 pinch 状态发送'1'/'0'。 - 观察舵机随 pinch / release 动作移动。
- 手追踪正常时,
4. 主要 Python 脚本说明(按功能)
4.1 手部追踪:hand_udp.py
功能总结:
-
打开摄像头,使用 MediaPipe Hands 检测多只手。
-
每一帧计算:
- 掌宽 / 掌长(像素)
- 卷曲度
curl(0~1) - 侧度
side(0~1) - 掌心/手背朝向
palm_front - 掌根深度
Z(米) - 20 条骨骼方向向量(单位向量)
pinch(拇指+食指是否捏合)
-
把上述信息打包成 JSON,UDP 发给:
- Unity 监听端口(默认 5065)
- Arduino UDP 桥接端口(默认 5066)
关键点:
-
标定逻辑
- 使用
CalibState结构存储:采样的掌宽列表 / 掌长列表、k_w、k_l等。 - 按下
c后采样一段时间,再让你输入真实距离,计算k_w = d_real * w_med之类的标定参数。 - 深度估计中,用
Z ≈ k_w / palm_width与Z ≈ k_l / palm_length为两条通道,再结合 curl / side 做加权融合。
- 使用
-
pinch 检测
- 一般是取
thumb_tip与index_tip的距离,小于阈值时认为在捏合。 - 直接写入
"pinch": true/false到 JSON。
- 一般是取
-
回调 on_payload(在
main.py中注册)- 可以拿到整帧 JSON,统计有多少只手在 pinch,并进行后处理(如串口输出)。
4.2 深度估计算法 Demo:hand_easy.py
用途是“把复杂的深度估计单独拎出来玩”,不牵涉到 UDP 和 Unity。
里面重点函数:
compute_palm_width_and_length(...)– 只要有 landmark,就能算出两个像素量,作为深度 proxy。compute_curl(...)– 根据手指关节夹角判断手是摊开还是握拳。compute_side(...)– 手是正对摄像头还是侧过来。fuse_depth(Zw, Zl, curl, side, palm_front, ...)– 用权重/修正项把两个深度通道融合成一个Z_final。draw_hud(...)– 在画面旁边打印所有中间量,方便调参理解。
4.3 文本模型链路
-
原始数据:
text_object_dataset.jsonl
每行一个 JSON:{"text": "...", "label": "101"},包含中英文。 -
collect_data.py:交互式添加数据。 -
clean_dataset.py:过滤掉含中文字符的样本,写入cleaned_text_object_dataset.jsonl。 -
train_model.py:从 cleaned 数据训练/增量训练一个SGDClassifier,保存到text_model.pkl,同时打印训练集指标。 -
run_model.py:命令行下测试模型,对输入句子打印 top-k label+prob。 -
text_infer_server.py:-
启动时加载模型一次。
-
作为 TCP 服务对外暴露:
- 每次收到一行文本 → 做一次推理 → 返回
"label|prob\n"。
- 每次收到一行文本 → 做一次推理 → 返回
-
4.4 综合入口:main.py
main.py 把三个方向的功能粘在一起:
-
手追踪 + UDP:从
tools.hand_udp启动手部追踪循环。 -
串口桥接:给
hand_udp注册一个回调on_payload(payload)- 在 JSON 里统计当前是否有手在 pinch。
- 若有 →
ser.write(b"1"),否则 →ser.write(b"0")。
-
文本 TCP 服务:用
TextInferHandler+ThreadedTCPServer在 9009 端口监听 Unity 发来的文本。
这样你只要跑一个 python main.py,就可以同时支撑 Unity 的手追踪 + 文本生成 + 硬件反馈。
4.5 UDP → 串口脚本:arduino_udp_receive.py
当你不想在 main.py 里混合太多逻辑时,可以单独跑这个脚本:
- UDP
5066收到与 Unity 相同格式的 JSON。 - 解析出当前的 pinch 状态。
- 若状态变化,向 Arduino 串口发送
'0'或'1'。 - 适合把“硬件桥接”独立出来做调试。
5. Unity 侧脚本说明
5.1 HandFromVectors.cs – UDP 收包 + 手骨重建
核心职责:
-
从指定端口(默认 5065)创建
UdpClient。 -
解析 Python 发来的 JSON:
wrist像素坐标 + 归一化坐标 +z_m(深度)。- 20 条骨骼方向向量(单位向量)。
pinch/is_left信息。
...
v1.0 Hands in Unity
This version successfully made hands to react and drag bodies in the Unity.
In Python we have lib called hand_easy.py. True running Python is hand_udp.py. Unity files under \Game\Assets\Scenes。