基于MediaPipeUnityPlugin的三维手势追踪

这篇具有很好参考价值的文章主要介绍了基于MediaPipeUnityPlugin的三维手势追踪。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

VID_20230421_005228

简述

目前比较好用的几种手势追踪,很多都是基于AR,需要硬件支持

而Google的MediaPipe则门槛比较低,用电脑摄像头也能跑
同时,又有MediaPipeUnityPlugin已经帮我们移植进了Unity

但 MediaPipe 是基于图像识别的,因此我手的往前往后,近大远小 在它看来只是在放大缩小,得到的数据都只发生在一个平面上,缺少硬件的支持所以没有深度信息。
幸运的是,即使只是一个平面,强大的Google至少帮我们做到了骨骼的相对深度
基于MediaPipeUnityPlugin的三维手势追踪

于是,本文要做的是,如最上面视频所示,模拟出手的深度,让我们的手真正在一个三维空间里面活动

如果你尚不了解 MediaPipeUnityPlugin,先看看我上一篇 MediaPipe-UnityPlugin的基本使用
如果你已经认识 MediaPipeUnityPlugin 的 HandTracking,可以直接跳到 [5.获取坐标] 环节


参考案例

Assets\MediaPipeUnity\Samples\Scenes\Hand Tracking\Hand Tracking.unity
基于MediaPipeUnityPlugin的三维手势追踪

虽说插件已经提供了完整的案例,但因为我们要在这基础上加入额外的功能,所以还是模仿着搭一个自己的Graph和Solution,同时去掉一些用不到的内容,以简化代码

1. 声明一个 MyGraph

public class MyHandTrackingGraph : GraphRunner
{
	public override void StartRun(ImageSource imageSource)
    {
    	// 此处进行输出流的启动 和 CalculatorGraph的StartRun
    }
    protected override IList<WaitForResult> RequestDependentAssets()
    {
    	// 此处加载用到的数据文件
    }
}
  • 首先声明我们的输出流 和 输出流的名称

输出流我们使用插件提供的 OutputStream<TPacket, TValue>
手部追踪的输出流有6种:
手掌检测基于手掌检测的矩形区手节点的坐标手节点的世界坐标基于坐标的矩形区左右手检测

分别如下:

// 手掌检测
OutputStream<DetectionVectorPacket, List<Detection>> _palmDetectionsStream;
const string _PalmDetectionsStreamName = "palm_detections";
// 基于手掌检测的矩形区
OutputStream<NormalizedRectVectorPacket, List<NormalizedRect>> _handRectsFromPalmDetectionsStream;
const string _HandRectsFromPalmDetectionsStreamName = "hand_rects_from_palm_detections";
// 手节点的坐标
OutputStream<NormalizedLandmarkListVectorPacket, List<NormalizedLandmarkList>> _handLandmarksStream;
const string _HandLandmarksStreamName = "hand_landmarks";
// 手节点的世界坐标
OutputStream<LandmarkListVectorPacket, List<LandmarkList>> _handWorldLandmarksStream;
const string _HandWorldLandmarksStreamName = "hand_world_landmarks";
// 基于坐标的矩形区
OutputStream<NormalizedRectVectorPacket, List<NormalizedRect>> _handRectsFromLandmarksStream;
const string _HandRectsFromLandmarksStreamName = "hand_rects_from_landmarks";
// 左右手检测
OutputStream<ClassificationListVectorPacket, List<ClassificationList>> _handednessStream;
const string _HandednessStreamName = "handedness";

还有我们输入流的名称

const string _InputStreamName = "input_video";

我们此次用到的只有手节点的坐标,所以只需要

const string _InputStreamName = "input_video";

OutputStream<NormalizedLandmarkListVectorPacket, List<NormalizedLandmarkList>> _handLandmarksStream;
const string _HandLandmarksStreamName = "hand_landmarks";
  • 随后在 ConfigureCalculatorGraph() 对它进行一下初始化和配置
protected override Status ConfigureCalculatorGraph(CalculatorGraphConfig config)
{
    _handLandmarksStream = new OutputStream<NormalizedLandmarkListVectorPacket, List<NormalizedLandmarkList>>(
        calculatorGraph, _HandLandmarksStreamName, config.AddPacketPresenceCalculator(_HandLandmarksStreamName), timeoutMicrosec);
        
    return base.ConfigureCalculatorGraph(config);
}

OutputStream的构造函数如下

/// <summary>
///   实例化一个 OutputStream class
///   图形必须具有 PacketPresenceCalculator 节点,用于计算流是否有输出
/// </summary>
/// <remarks>
///   当您希望同步获取输出,但不希望在等待输出时阻塞线程时,这很有用
/// </remarks>
/// <param name="calculatorGraph"> 流的所有者 </param>
/// <param name="streamName"> 输出流的名称 </param>
/// <param name="presenceStreamName"> 当输出存在时,输出true的流的名称 </param>
/// <param name="timeoutMicrosec"> 如果输出数据包为空,则 OutputStream 实例会丢弃数据包,直到此处指定的时间结束 </param>
public OutputStream(CalculatorGraph calculatorGraph, string streamName, string presenceStreamName, long timeoutMicrosec = 0) : this(calculatorGraph, streamName, false, timeoutMicrosec)
{
    this.presenceStreamName = presenceStreamName;
}
  • 然后定义一个异步输出监听的接口 和 一个获取同步输出的接口
public event EventHandler<OutputEventArgs<List<NormalizedLandmarkList>>> OnHandLandmarksOutput
{
    add => _handLandmarksStream.AddListener(value);
    remove => _handLandmarksStream.RemoveListener(value);
}

public bool TryGetNext(out List<NormalizedLandmarkList> handLandmarks, bool allowBlock = true)
{
    var currentTimestampMicrosec = GetCurrentTimestampMicrosec();
    return TryGetNext(_handLandmarksStream, out handLandmarks, allowBlock, currentTimestampMicrosec);
}
  • 在 StartRun() 和 Stop() 中进行启动和释放
public override void StartRun(ImageSource imageSource)
{
    _handLandmarksStream.StartPolling().AssertOk();
    calculatorGraph.StartRun().AssertOk();
}

public override void Stop()
{
    _handLandmarksStream?.Close();
    _handLandmarksStream = null;
    base.Stop();
}
  • 然后定义一个输入接口,用于将输入源传给输入流
public void AddTextureFrameToInputStream(TextureFrame textureFrame)
{
    AddTextureFrameToInputStream(_InputStreamName, textureFrame);
}
  • 至此,输入输出已经写好,我们的Graph目前应该是像这样
public class MyHandTrackingGraph : GraphRunner
{
    const string _InputStreamName = "input_video";

    OutputStream<NormalizedLandmarkListVectorPacket, List<NormalizedLandmarkList>> _handLandmarksStream;
    const string _HandLandmarksStreamName = "hand_landmarks";
    
    public event EventHandler<OutputEventArgs<List<NormalizedLandmarkList>>> OnHandLandmarksOutput
    {
        add => _handLandmarksStream.AddListener(value);
        remove => _handLandmarksStream.RemoveListener(value);
    }
    
    public bool TryGetNext(out List<NormalizedLandmarkList> handLandmarks, bool allowBlock = true)
    {
        var currentTimestampMicrosec = GetCurrentTimestampMicrosec();
        return TryGetNext(_handLandmarksStream, out handLandmarks, allowBlock, currentTimestampMicrosec);
    }
    
    public override void StartRun(ImageSource imageSource)
    {
        _handLandmarksStream.StartPolling().AssertOk();
        calculatorGraph.StartRun().AssertOk();
    }

    public override void Stop()
    {
        _handLandmarksStream?.Close();
        _handLandmarksStream = null;
        base.Stop();
    }
    
    public void AddTextureFrameToInputStream(TextureFrame textureFrame)
    {
        AddTextureFrameToInputStream(_InputStreamName, textureFrame);
    }
    
    protected override Status ConfigureCalculatorGraph(CalculatorGraphConfig config)
    {
        _handLandmarksStream = new OutputStream<NormalizedLandmarkListVectorPacket, List<NormalizedLandmarkList>>(
            calculatorGraph, _HandLandmarksStreamName, config.AddPacketPresenceCalculator(_HandLandmarksStreamName), timeoutMicrosec);
        
        return base.ConfigureCalculatorGraph(config);
    }

    protected override IList<WaitForResult> RequestDependentAssets()
    {
        
    }
}

还差一个 RequestDependentAssets()
这里加载我们手坐标检测的数据文件 “hand_landmark_full.bytes”,它会通过我们在Bootstrap选择的加载方式去找到这个文件

protected override IList<WaitForResult> RequestDependentAssets()
{
    return new List<WaitForResult>()
    {
        WaitForAsset("hand_landmark_full.bytes"),
    };
}

2. 声明一个 MySolution

public class MyHandTrackingSolution : ImageSourceSolution<MyHandTrackingGraph>
{
    protected override void OnStartRun()
    {
        // 此处绑定graph的输出监听
    }

    protected override void AddTextureFrameToInputStream(TextureFrame textureFrame)
    {
        // 此处接入到graph的输入接口
    }

    protected override IEnumerator WaitForNextValue()
    {
        // 此处获取graph的同步输出
    }
}
  • 首先我们声明一个回调,绑定到graph的 OnHandLandmarksOutput
    (这里的graphRunner即我们的MyHandTrackingGraph)
protected override void OnStartRun()
{
    graphRunner.OnHandLandmarksOutput += OnHandLandmarksOutput;
}
void OnHandLandmarksOutput(object stream, OutputEventArgs<List<NormalizedLandmarkList>> eventArgs)
{
	var handLandmarks = eventArgs.value;
    // 这里对得到的数据进行处理
}
  • 然后将图像输入传给graph
protected override void AddTextureFrameToInputStream(TextureFrame textureFrame)
{
    graphRunner.AddTextureFrameToInputStream(textureFrame);
}
  • 再获取graph的同步输出

这里我们区分一下,同步有两个模式,阻塞同步 和 不阻塞同步,由参数 runningMode 决定(定义在ImageSourceSolution)

protected override IEnumerator WaitForNextValue()
{
    List<NormalizedLandmarkList> handLandmarks = null;
    if (runningMode == RunningMode.Sync)
    {
        graphRunner.TryGetNext(out handLandmarks, true));
    }
    else if (runningMode == RunningMode.NonBlockingSync)
    {
        yield return new WaitUntil(() => graphRunner.TryGetNext(out handLandmarks, false));
    }
    
    // 这里对得到的数据进行处理
}

好了,我们的Solution这样就完事了,得到的数据怎么用,我们后面再说


3. 场景搭建

新建个场景,起个空节点来挂脚本,就叫它Main吧
基于MediaPipeUnityPlugin的三维手势追踪
挂上我们的Graph和Solution,并配置
基于MediaPipeUnityPlugin的三维手势追踪
这里的3个Config直接使用预设好的,位于Assets\MediaPipeUnity\Samples\Scenes\Hand Tracking
基于MediaPipeUnityPlugin的三维手势追踪

可以看到我们还差Bootstrap、Screen 和 TextureFramePool

Bootstrap 和 TextureFramePool 直接也挂在这下面即可
基于MediaPipeUnityPlugin的三维手势追踪

Screen 则新建一个UI/RawImage,挂在它下面,并简单设置一下Rect
基于MediaPipeUnityPlugin的三维手势追踪

因为我们选择是的WebCamera作为输入源,所以还得加一个 WebCamSource,也挂在Main下面即可
基于MediaPipeUnityPlugin的三维手势追踪
Default Width是画面的分辨率,调高了会卡,按默认的1280即可
但因此我们的Screen会缩放成1280,所以我们给Screen加个AutoFit让它全屏
基于MediaPipeUnityPlugin的三维手势追踪

  • 至此,我们可以尝试运行了

4. MyGraph 其二

好的 画面是出来了,但不出所料的报错了
基于MediaPipeUnityPlugin的三维手势追踪
我们看看来源,发生在 Graph 中 StartRun 的 calculatorGraph.StartRun().AssertOk()

再看看报错,我们用到的 config 是带有SidePacket的,所以我们还得给他整一个
(上篇讲 CalculatorGraph 时提到过一下)

var sidePacket = new SidePacket();
calculatorGraph.StartRun(sidePacket).AssertOk();

怎么配置呢,有接口

sidePacket.Emplace("packet名", new xxxPacket());

名字哪来,我们来打开 hang_tracking_cpu.txt 看看
在下面找到了我们的 side_packet,当然 直接看报错也能得知
基于MediaPipeUnityPlugin的三维手势追踪
我们直接从案例中搬运一下,其中 SetImageTransformationOptions() 是GraphRunner提供的接口,封装了前面3个SidePacket的配置,完事我们的GraphRunner也有提供 StartRun 的接口把SidePacket传进去

var sidePacket = new SidePacket();
SetImageTransformationOptions(sidePacket, imageSource, true); // GraphRunner提供的接口
sidePacket.Emplace("model_complexity", new IntPacket(1);
sidePacket.Emplace("num_hands", new IntPacket(2));
//calculatorGraph.StartRun(sidePacket).AssertOk();
StartRun(sidePacket);
  • 好的,现在已经不报错了,但画面好像反了,我们给Screen翻转一下

基于MediaPipeUnityPlugin的三维手势追踪

  • 尝试伸个手,在异步回调里获取一下输出

异步获取需要在solution上的runningMode选择异步
基于MediaPipeUnityPlugin的三维手势追踪

void OnHandLandmarksOutput(object stream, OutputEventArgs<List<NormalizedLandmarkList>> eventArgs)
{
    if (eventArgs.value != null) Debug.Log("--- Recv ---");
}

当然同步也行
基于MediaPipeUnityPlugin的三维手势追踪

protected override IEnumerator WaitForNextValue()
{
    List<NormalizedLandmarkList> handLandmarks = null;
    if (runningMode == RunningMode.Sync)
    {
        graphRunner.TryGetNext(out handLandmarks, true));
    }
    else if (runningMode == RunningMode.NonBlockingSync)
    {
        yield return new WaitUntil(() => graphRunner.TryGetNext(out handLandmarks, false));
    }
    
    if (handLandmarks != null) Debug.Log("--- Recv ---");
}

基于MediaPipeUnityPlugin的三维手势追踪

非常好,可以进行下一步了


5. 获取坐标

已知MediaPipe手势追踪的骨骼有21个节点,如下图

基于MediaPipeUnityPlugin的三维手势追踪

首先我们拿到的是 List<NormalizedLandmarkList>
遍历下来数量有0到2个,这是识别到手的数量,每只手一个NormalizedLandmarkList

它下面有个 Landmark,也是集合类型
遍历出来的是NormalizedLandmark,数量是21,对应手的骨骼节点,下面有3个属性:X、Y、Z,显然这是坐标
由此可得

if (eventArgs.value != null)
{
	var handLandmarks = eventArgs.value;
	foreach(var normalizedLandmarkList in handLandmarks)
	{
		var landmarks = normalizedLandmarkList.Landmark;
		foreach(var landmark in landmarks)
		{
		    var pos = new Vector3(landmark.X, landmark.Y, landmark.Z);
		}
	}
}

6. 可视化

我们预设一个小球体,运行时生成21个对齐到我们这些坐标点上
顺便加上碰撞体和刚体,让它能与场景物体交互(刚体记得去掉重力)
基于MediaPipeUnityPlugin的三维手势追踪

新建一个脚本来控制它,就叫 MyView 吧

绑定我们的预制体并在 Start 中提前生成好

public Transform objRoot; // 存放小球的父节点
public GameObject boneObj;
List<GameObject> m_boneObjList;

void Start()
{
	m_boneObjList= new List<GameObject>();
	while (m_boneObjList.Count < 21)
	{
	    m_boneObjList.Add(Instantiate(boneObj, objRoot));
	}
}

然后我们在 Update 中把坐标赋进去即可

void Update()
{
	var landmarks = ... //上面拿到的landmarks
	for (int i = 0; i < landmarks.Count; i++)
    {
        var mark = landmarks[i];
        var pos = new Vector3(mark.X, mark.Y, mark.Z);
        m_boneObjList[i].transform.localPosition = pos;
    }
}

在场景中挂上MyView,运行一下

基于MediaPipeUnityPlugin的三维手势追踪

好像反了,我们给x和y翻转一下

var pos = new Vector3(-mark.X, -mark.Y, mark.Z);

基于MediaPipeUnityPlugin的三维手势追踪

好的没问题,可能看起来有点小,我们给pos乘一个倍数放大一下即可

我们把坐标获取从MySolution搬到MyView,并简单封装一下

public Transform objRoot; // 存放小球的父节点
public GameObject boneObj;
List<GameObject> m_boneObjList;

List<NormalizedLandmarkList> m_currList;
bool m_newLandMark = false;

void Start()
{
	m_boneObjList= new List<GameObject>();
	while (m_boneObjList.Count < 21)
	{
	    m_boneObjList.Add(Instantiate(boneObj, objRoot));
	}
}

public void DrawLater(List<NormalizedLandmarkList> list)
{
    m_currList = list;
    m_newLandMark = true;
}

public void DrawNow(List<NormalizedLandmarkList> list)
{
	if (list.Count == 0) return;
    var landmarks = list[0].Landmark; // 先忽略多只手的情况,只处理第一只手
    if (landmarks.Count <= 0) return;

    for (int i = 0; i < landmarks.Count; i++)
    {
        var mark = landmarks[i];
        var pos = new Vector3(mark.X, mark.Y, mark.Z);
        m_boneObjList[i].transform.localPosition = pos;
    }
}

void LateUpdate()
{
    if (m_newLandMark) UpdateDraw();
}

void UpdateDraw()
{
    m_newLandMark = false;
    DrawNow(m_currList);
}

这里区分了同步和异步的处理,同步数据直接接入 DrawNow(),异步数据则接入到 DrawLater(),稍后从Update里处理,因为它不允许在异步线程里面操作。

现在只有几个骨骼点看起来不太直观,我们用LineRenderer给他加几条Line
琢磨一下5条够了,对应的骨骼点如下
基于MediaPipeUnityPlugin的三维手势追踪基于MediaPipeUnityPlugin的三维手势追踪

我们记录一下这些骨骼点,并绘制出来

public LineRenderer[] lines;

readonly int[][] m_connections = {
    new []{0, 1, 2, 3, 4},
    new []{0, 5, 6, 7, 8},
    new []{9, 10, 11, 12},
    new []{13, 14, 15, 16},
    new []{0, 17, 18, 19, 20},
};

void DrawLine()
{
    for (int i = 0; i < m_connections.Length; i++)
    {
        var connections = m_connections[i];
        var pos = new Vector3[connections.Length];
        for (int j = 0; j < connections.Length; j++)
        {
            pos[j] = m_boneObjList[connections[j]].transform.position;
        }

        lines[i].positionCount = pos.Length;
        lines[i].SetPositions(pos);
    }
}

记得在外面绑定引用哦,来看看效果
基于MediaPipeUnityPlugin的三维手势追踪


7. 深度模拟

说了这么多前期工夫,终于来到我们的核心环节了

先来说一下思路,我们主要要做两件事:
– ①求得手的深度,也就是前后移动的距离
– ②把“放大/缩小”了的手还原回原来的大小

实现方式也很简单粗暴,我们不奢求精确值,只求个大概:
1、我们找两个掌心附近的相邻骨骼点,如[0]和[1],因为这里的点之间的相对位置比较稳定。
2、我们取他们的距离distance,然后某一时刻的值为初始值[d0];然后取之后某个位置的值为[d1]。
3、这个距离是会随着远近而一起放大缩小的,于是能得到他们的缩放比 [d0] / [d1] 。
4、如此一来,②就能完成了,将之后的坐标乘以这个缩放,自然也就恢复了原来的大小。
5、得到了缩放比,那①也好办,我们直接设定一个系数,让缩放比乘以这个系数,则为深度值

听着是不是过于简单粗暴哈哈,没法,我们手头上能用到的数据,只能办到这样,
对初始值和系数进行慢慢调整后,也能得到非常仿真的效果。

首先,我们定义两个常量作为初始距离和深度系数

static float m_baseDist = 0.5f;
static float m_depthRatio = 30f;

然后如上所述进行处理得到缩放比scale和深度值depth

void GetDepthByLandmark(NormalizedLandmark mark1, NormalizedLandmark mark2, out float depth, out float scale)
{
    var pos1 = new Vector3(mark1.X, mark1.Y, mark1.Z);
    var pos2 = new Vector3(mark2.X, mark2.Y, mark2.Z);
    var length = Vector3.Distance(pos1, pos2);
    scale = m_baseDist / length;
    depth = scale * m_depthRatio;
}

随后我们把得到的值直接给到 objRoot

GetDepthByLandmark(landmarks[0], landmarks[1], out var depth, out var scale);

var rootPos = objRoot.localPosition;
objRoot.localPosition = new Vector3(rootPos.x, rootPos.y, depth);
objRoot.localScale = new Vector3(scale, scale, scale);

至此,我们的 MyView 应该是像这样

public class MyView : MonoBehaviour
{
	public Transform objRoot; // 存放小球的父节点
	public GameObject boneObj;
	List<GameObject> m_boneObjList;

	public LineRenderer[] lines;
	
	List<NormalizedLandmarkList> m_currList;
	bool m_newLandMark = false;
	
	void Start()
	{
		m_boneObjList= new List<GameObject>();
		while (m_boneObjList.Count < 21)
		{
		    m_boneObjList.Add(Instantiate(boneObj, objRoot));
		}
	}
	
	public void DrawLater(List<NormalizedLandmarkList> list)
	{
	    m_currList = list;
	    m_newLandMark = true;
	}
	
	public void DrawNow(List<NormalizedLandmarkList> list)
	{
		if (list.Count == 0) return;
	    var landmarks = list[0].Landmark; // 先忽略多只手的情况,只处理第一只手
	    if (landmarks.Count <= 0) return;
	
		GetDepthByLandmark(landmarks[0], landmarks[1], out var depth, out var scale);
		var rootPos = objRoot.localPosition;
		objRoot.localPosition = new Vector3(rootPos.x, rootPos.y, depth);
		objRoot.localScale = new Vector3(scale, scale, scale);

	    for (int i = 0; i < landmarks.Count; i++)
	    {
	        var mark = landmarks[i];
	        var pos = new Vector3(mark.X, mark.Y, mark.Z);
	        m_boneObjList[i].transform.localPosition = pos * 14; // 我这里要14倍才看起来不那么小
	    }
	}
	
	void LateUpdate()
	{
	    if (m_newLandMark) UpdateDraw();
	}
	
	void UpdateDraw()
	{
	    m_newLandMark = false;
	    DrawNow(m_currList);
	}

	readonly int[][] m_connections = {
        new []{0, 1, 2, 3, 4},
        new []{0, 5, 6, 7, 8},
        new []{9, 10, 11, 12},
        new []{13, 14, 15, 16},
        new []{0, 17, 18, 19, 20},
    };

    void DrawLine()
    {
        for (int i = 0; i < m_connections.Length; i++)
        {
            var connections = m_connections[i];
            var pos = new Vector3[connections.Length];
            for (int j = 0; j < connections.Length; j++)
            {
                pos[j] = m_boneObjList[connections[j]].transform.position;
            }

            lines[i].positionCount = pos.Length;
            lines[i].SetPositions(pos);
        }
    }

	static float m_baseDist = 0.5f;
	static float m_depthRatio = 30f;
	void GetDepthByLandmark(NormalizedLandmark mark1, NormalizedLandmark mark2, out float depth, out float scale)
	{
	    var pos1 = new Vector3(mark1.X, mark1.Y, mark1.Z);
	    var pos2 = new Vector3(mark2.X, mark2.Y, mark2.Z);
	    var length = Vector3.Distance(pos1, pos2);
	    scale = m_baseDist / length;
	    depth = scale * m_depthRatio;
	}
}

运行一下

基于MediaPipeUnityPlugin的三维手势追踪
对味了,觉得有点歪的话也可以调一下objRoot的位置


8. 加点交互

我们往场景里面加点物体,给要交互的物体加上碰撞体和刚体

基于MediaPipeUnityPlugin的三维手势追踪

至此,我们已达到了视频中的效果

VID_20230421_005228


我们目前只处理了一只手的情况,要双手的话,我们用两份的小球和Line同样操作即可

改进思路:
有条件的话还是来点硬件支持,如 ARCore 的 Depth API ,它可以帮我们获取到画面上某个坐标的深度,Unity的AR包里就有。
目前正在尝试,成功的话我后续写一篇…文章来源地址https://www.toymoban.com/news/detail-479523.html

到了这里,关于基于MediaPipeUnityPlugin的三维手势追踪的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity Meta Quest 一体机开发(十二):【手势追踪】Poke 交互 - 用手指点击由 3D 物体制作的 UI 按钮

    此教程相关的详细教案,文档,思维导图和工程文件会放入 Spatial XR 社区 。这是一个高质量 XR 社区,博主目前在内担任 XR 开发的讲师。此外,该社区提供教程答疑、及时交流、进阶教程、外包、行业动态等服务。 社区链接: Spatial XR 高级社区(知识星球) Spatial XR 高级社区

    2024年02月04日
    浏览(81)
  • Unity Meta Quest 一体机开发(三):【手势追踪】Oculus Integration/Meta XR SDK 基本原理、概念与结构+玩家角色基本配置

    此教程相关的详细教案,文档,思维导图和工程文件会放入 Spatial XR 社区 。这是一个高质量知识星球 XR 社区,博主目前在内担任 XR 开发的讲师。此外,该社区提供教程答疑、及时交流、进阶教程、外包、行业动态等服务。 社区链接: Spatial XR 高级社区(知识星球) Spatial

    2024年01月16日
    浏览(68)
  • C/C++每日一练(20230421)

    目录 1. 位1的个数  🌟 2. 递归和非递归求和  ※ 3. 俄罗斯套娃信封问题  🌟🌟🌟 🌟 每日一练刷题专栏 🌟 Golang每日一练 专栏 Python每日一练 专栏 C/C++每日一练 专栏 Java每日一练 专栏 编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中

    2023年04月24日
    浏览(25)
  • MediaPipeUnityPlugin(最新版)摇摆拳人脸识别

    目前是MediaPipeUnity.0.12.0.unitypackage 1、打开Bootstrap,图像源改成Video,把Solution拖拽到FaceDetctionSolution的Bootstrap上 2、FaceDetectionGraph的ModelType改成FullRangeSparse 3、VideoSource添加视频 运行效果 摇摆拳Unity人脸识别

    2024年01月16日
    浏览(34)
  • 基于Mediapipe的Python手势识别项目(手势识别游戏)附项目源码

    基于Mediapipe的手势识别,完成的手势识别游戏。运行效果图如下: 首先是初始的界面效果图: 游戏规则:屏幕上会出现蚊子和蜜蜂,当手蜷曲握起时,表示抓的动作。如果抓到右边移动的蚊子,则会增加分数,如果抓到右边的蜜蜂,则会出现被蛰到的声音 :) 调用Mediapip

    2024年02月11日
    浏览(100)
  • 基于FPGA的手势识别

    使用正点原子开拓者开发板,预定义三种手势:石头(0)、剪刀(2)、布(5)。通过 OV5640 摄像头套件对手势图像进行采集,LCD 显示屏(显示屏用的正点原子的 7 寸 RGB_LCD,分辨率为 1024×600)对系统处理后的手势进行实时显示,根据预定义手势的面积周长比判断手势,最终

    2024年02月11日
    浏览(40)
  • 基于opencv的手势识别

    大家好,我是一名本科生,我的主要学习方向是计算机视觉以及人工智能。按照目前的学习进度来说,我就是一小白,在这里写下自己编写的程序,与大家分享,记录一下自己的成长。 思路分析 获取图片,在图片中找到手,然后进行一系列的闭运算,降噪平滑处理,轮廓查

    2024年02月03日
    浏览(55)
  • 基于mediapipe的手势数字识别

    基于mediapipe识别手势所对应的数字(一、二、三、四、五)。 mediapipe的官网 总体思路 :mediapipe可以识别手掌的关键点,我的思路是识别单根手指是否弯曲,然后根据五根手指的弯曲程度判断手势所对应的数字。 那怎么判断单根手指是否弯曲呢? 我是根据手指的四个关键点的相

    2024年02月11日
    浏览(53)
  • 基于计算机视觉的手势识别技术

    一个不知名大学生,江湖人称菜狗 original author: Jacky Li Email : 3435673055@qq.com Time of completion:2023.5.2 Last edited: 2023.5.2 手语是一种主要由听力困难或耳聋的人使用的交流方式。这种基于手势的语言可以让人们轻松地表达想法和想法,克服听力问题带来的障碍。 这种便捷的交流方式

    2024年02月04日
    浏览(46)
  • MATLAB基于卷积神经网络的手势识别

    目录 1. 数据集介绍  2. 训练、保存网络 3. 手势识别 4. 识别结果 5. 总结 本实验所用数据集为从Kaggle平台下载的手语数据集(sign_mnist)中选取的部分数据。 sign_mnist 数据集格式的模式化与经典 MNIST 紧密匹配。每个训练和测试用例表示一个标签 (0-25),作为每个字母 A-Z 的一

    2024年02月06日
    浏览(106)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包