资讯详情

30分钟内基于激光雷达的手部姿态估计

点击上方“3D视觉车间,选择星标

第一时间送达干货

95292d74bb7e3b85fc43ba0576c4e2a1.png

作者!

来源、深度学习和计算机视觉

介绍

大家好!虽然赛博朋克还没有进入我们的生活,神经界面也远不理想,但激光雷达可以成为机械手未来道路的第一阶段。因此,为了在假期不感到无聊,我决定幻想计算机控制,可能有任何设备,如挖掘机、宇宙飞船、无人机或炉子。

主要思想是移动鼠标,而不是移动整只手,而是移动食指,这将使你浏览菜单,按下按钮,成为一个真正的键盘忍者,而不离开键盘!如果添加滑动或滚动手势会发生什么?我想会有炸弹的!(但我们还需要等待几年)

让我们开始组装我们未来机械手的原型

你需要:

  1. 带有 LiDAR Intel Realsense L515 的摄像头。

  2. 能够在python中编程

  3. 记住学校数学

  4. 安装在监视器上的相机也被称为三脚架

我们用全球速卖通的三脚架把相机固定在上面,结果证明非常方便,轻便便宜

我们知道如何制作原型

完成这项任务的方法有很多。你可以训练自己的探测器或手部分割,切割右手的结果图像,然后把这个从 Facebook 图像中应用了精彩的存储库,取得了优异的效果。

在阅读了这个链接后,你可以知道这是当今最好的选择之一:https://ai.googleblog.com/2019/08/on-device-real-time-hand-tracking-with.html

首先,一切都已经开箱即用了 - 需要安装和启动 30 考虑到所有的先决条件,分钟。

其次,由于强大的开发团队,他们不仅采用了手部姿势估计的最新技术,而且提供了易于理解的技术 API。

第三,网络已经准备好了 CPU 因此,进入门槛极低。

你可能会问我为什么不使用获胜者的存储库。

事实上,我已经详细研究了他们的解决方案,他们已经准备好了,没有的数百万网格。但在我看来,最大的问题是他们处理深度图像。

因为他们是学者,他们毫不犹豫地通过Matlab此外,拍摄深度的分辨率在我看来很小。这可能会对结果产生深远的影响。

因此,最简单的方法似乎是获得它RGB图片中的关键点,并通过XY坐标取深帧中沿Z轴的值。目前的任务不是优化太多的东西。因此,我们将从开发的角度进行优化,因为它更快。

记住学校数学

正如我所写,为了获得鼠标光标所在点的坐标,我们需要建立一条通过手指指骨的两个关键点的线,并找到线与点的交点显示器的平面。

图片显示了显示器的平面和与它相交的线。您可以在这里查看数学。

使用两点,我们得到空间中直线的参数表示。

我不会太关注学校的数学课程。

用相机安装的库

这可能是这项工作中最困难的部分。事实证明,Ubuntu 相机软件非常粗糙,充满了各种各样的bug。

到目前为止,我还没有打败相机的奇怪行为,有时它在启动时不加载参数。

重启计算机后,相机只能工作一次!但有一个解决方案:每次启动前,软件硬重置相机,重置相机USB,也许一切都会好起来。

对了,对了 Windows 一切都很好。然而,开发人员想象一个基于它的人 Windows 机器人很奇怪。

要真正了解 Ubuntu 请执行以下操作:

$sudoapt-getinstalllibusb-1.0-0-dev Thenreruncmakeandmakeinstall.Hereisacompleterecipethatworkedforme: $sudoapt-getinstalllibusb-1.0-0-dev $gitclonehttps://github.com/IntelRealSense/librealsense.git $cdlibrealsense/ $mkdirbuild&&cdbuild

通过分类收集,或多或少会稳定。一个月的技术支持沟通表明您需要安装Ubuntu16,否则会遭受痛苦。懂的都懂。

继续了解神经网络的复杂性

现在让我们看看另一个手指和鼠标操作的视频。请注意,指针不能停留在一个地方,而是在预定点周围漂浮。同时,我可以很容易地指向我需要的单词,但对于一个字母,它更困难,我必须小心地移动光标:

正如你所理解的,这不是和我握手,而是从激光雷达中获得的,关于基于值的关键点和 Z 坐标不断波动。

让我们仔细看看:

在我们的媒体管道中 SOTA 中,波动肯定较少,但它们也存在。事实证明,他们通过在当前帧和训练网络中使用过去帧热图中的 prokid vaniya 解决这个问题——它提供了更多的稳定性,但不是 100%的稳定。

另外,在我看来,标记的特殊性也起作用。这么多帧几乎不可能做同样的标记,更何况帧的分辨率到处都不一样,也不是很大。

另外,我们没有看到光的闪烁,这可能不是恒定的,因为操作时间不同于相机曝光。网络也从热图中返回一个sandwich ,它等于屏幕上关键点的数量,这个张的大小是 BxNx96x96,其中 N 是关键点的数量,当然,在阈值和调整到原始帧大小后,我们得到:

渲染热图示例:

代码审查

所有代码都在这个存储库中,而且非常短。让我们看看主文件,然后自己看看其余的。

importcv2 importmediapipeasmp importnumpyasnp importpyautogui importpyrealsense2.pyrealsense2asrs fromgoogle.protobuf.json_formatimportMessageToDict frommediapipe.python.solutions.drawing_utilsimport_normalized_to_pixel_coordinates frompynputimportkeyboard fromutils.commonimportget_filtered_values,draw_cam_out,get_right_index fromutils.hard_resetimporthardware_reset fromutils.set_optionsimportset_short_range pyautogui.FAILSAFE=False mp_drawing=mp.solutions.drawing_utils mp_hands=mp.solutions.hands #HandPoseEstimation hands=mp_hands.Hands(max_num_hands=2,min_detection_confidence=0.9) defon_press(key): ifkey==keyboard.Key.ctrl: pyautogui.leftClick() ifkey==keyboard.Key.alt: pyautogui.rightClick() defget_color_depth(pipeline,align,colorizer): frames=pipeline.wait_for_frames(timeout_ms=15000)#waitingforaframefromthecamera aligned_fraes = align.process(frames) 
    depth_frame = aligned_frames.get_depth_frame()
    color_frame = aligned_frames.get_color_frame()
if not depth_frame or not color_frame:
return None, None, None
    depth_ima = np.asanyarray(depth_frame.get_data())
    depth_col_img = np.asanyarray(colorizer.colorize(depth_frame).get_data())
    color_image = np.asanyarray(color_frame.get_data())
    depth_col_img = cv2.cvtColor(cv2.flip(cv2.flip(depth_col_img, 1), 0), cv2.COLOR_BGR2RGB)
    color_img = cv2.cvtColor(cv2.flip(cv2.flip(color_img, 1), 0), cv2.COLOR_BGR2RGB)
    depth_img = np.flipud(np.fliplr(depth_img))
    depth_col_img = cv2.resize(depth_col_img, (1280 * 2, 720 * 2))
    col_img = cv2.resize(col_img, (1280 * 2, 720 * 2))
    depth_img = cv2.resize(depth_img, (1280 * 2, 720 * 2))
return color_image, depth_color_image, depth_image
def get_right_hand_coords(color_image, depth_color_image):
    color_image.flags.writeable = False
    results = hands.process(color_image)
    color_image.flags.writeable = True
    color_image = cv2.cvtColor(color_image, cv2.COLOR_RGB2BGR)
    handedness_dict = []
    idx_to_coordinates = {}
    xy0, xy1 = None, None
if results.multi_hand_landmarks:
for idx, hand_handedness in enumerate(results.multi_handedness):
            handedness_dict.append(MessageToDict(hand_handedness))
        right_hand_index = get_right_index(handedness_dict)
if right_hand_index != -1:
for i, landmark_list in enumerate(results.multi_hand_landmarks):
if i == right_hand_index:
                    image_rows, image_cols, _ = color_image.shape
for idx, landmark in enumerate(landmark_list.landmark):
                        landmark_px = _normalized_to_pixel_coordinates(landmark.x, landmark.y,
                                                                       image_cols, image_rows)
if landmark_px:
                            idx_to_coordinates[idx] = landmark_px
for i, landmark_px in enumerate(idx_to_coordinates.values()):
if i == 5:
                    xy0 = landmark_px
if i == 7:
                    xy1 = landmark_px
break
return col_img, depth_col_img, xy0, xy1, idx_to_coordinates
def start():
    pipeline = rs.pipeline() # initialize librealsense
    config = rs.config() 
    print("Start load conf")
    config.enable_stream(rs.stream.depth, 1024, 768, rs.format.z16, 30)
    config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30)
    profile = pipeline.start(config) 
depth_sensor = profile.get_device (). first_depth_sensor ()
    set_short_range (depth_sensor) # load parameters for working at a short distance
    colorizer = rs.colorizer ()
    print ("Conf loaded")
    align_to = rs.stream.color
    align = rs.align (align_to) # combine depth map and color image
    try:
        while True:
            col_img, depth_col_img, depth_img = get_col_depth (pipelin, align, colorize)
            if color_img is None and color_img is None and color_img is None:
                continue
            color_img, depth_col_img, xy00, xy11, idx_to_coordinates = get_right_hand_coords (col_img,
                                                                                                 depth_col_img)
            if xy00 is not None or xy11 is not None:
                z_val_f, z_val_s, m_xy, c_xy, xy00_f, xy11_f, x, y, z = get_filtered_values (depth_img, xy00, xy11)
                pyautogui.moveTo (int (x), int (3500 - z)) # 3500 hardcode specific to my monitor
                if draw_cam_out (col_img, depth_col_img, xy00_f, xy11_f, c_xy, m_xy):
                    break
    finally:
        hands.close ()
        pipeline.stop ()
hardware_reset () # reboot the camera and wait for it to appear
listener = keyboard.Listener (on_press = on_press) # set a listener for keyboard button presses
listener.start ()
start () # start the program

我没有使用类或流,因为对于这样一个简单的情况,在无限的 while 循环中执行主线程中的所有内容就足够了。

一开始,媒体管道、相机被初始化,短距离和辅助变量的相机设置被加载。

接下来是称为“alight depth to color”的魔法——这个函数匹配RGB图像中的每个点,深度帧上的一个点,也就是说,它让我们有机会获得XY坐标,Z值。

据了解,需要在你的显示器上进行校准。我特意没有把这些参数单独拎出来,让决定运行代码的读者自己做,同时在代码中重复使用。

接下来,我们从整个预测中仅提取右手编号为 5 和 7 的点。

剩下要做的就是使用移动平均值过滤获得的坐标。当然,可以应用更严格的过滤算法,但是在查看了它们的可视化并拉动了各种杠杆之后,很明显深度为 5 帧的移动平均线对于演示就足够了,我想指出的是对于 XY,2-3 帧就足够了。但 Z 的情况更糟。

deque_l = 5
x0_d = collections.deque(deque_l * [0.], deque_l)
y0_d = collections.deque(deque_l * [0.], deque_l)
x1_d = collections.deque(deque_l * [0.], deque_l)
y1_d = collections.deque(deque_l * [0.], deque_l)
z_val_f_d = collections.deque(deque_l * [0.], deque_l)
z_val_s_d = collections.deque(deque_l * [0.], deque_l)
m_xy_d = collections.deque(deque_l * [0.], deque_l)
c_xy_d = collections.deque(deque_l * [0.], deque_l)
x_d = collections.deque(deque_l * [0.], deque_l)
y_d = collections.deque(deque_l * [0.], deque_l)
z_d = collections.deque(deque_l * [0.], deque_l)
def get_filtered_values(depth_image, xy0, xy1):
global x0_d, y0_d, x1_d, y1_d, m_xy_d, c_xy_d, z_val_f_d, z_val_s_d, x_d, y_d, z_d
    x0_d.append(float(xy0[1]))
    x0_f = round(mean(x0_d))
    y0_d.append(float(xy0[0]))
    y0_f = round(mean(y0_d))
    x1_d.append(float(xy1[1]))
    x1_f = round(mean(x1_d))
    y1_d.append(float(xy1[0]))
    y1_f = round(mean(y1_d))
    z_val_f = get_area_mean_z_val(depth_image, x0_f, y0_f)
    z_val_f_d.append(float(z_val_f))
    z_val_f = mean(z_val_f_d)
    z_val_s = get_area_mean_z_val(depth_image, x1_f, y1_f)
    z_val_s_d.append(float(z_val_s))
    z_val_s = mean(z_val_s_d)
    points = [(y0_f, x0_f), (y1_f, x1_f)]
    x_coords, y_coords = zip(*points)
    A = np.vstack([x_coords, np.ones(len(x_coords))]).T
    m, c = lstsq(A, y_coords)[0]
    m_xy_d.append(float(m))
    m_xy = mean(m_xy_d)
    c_xy_d.append(float(c))
    c_xy = mean(c_xy_d)
    a0, a1, a2, a3 = equation_plane()
    x, y, z = line_plane_intersection(y0_f, x0_f, z_v_s, y1_f, x1_f, z_v_f, a0, a1, a2, a3)
    x_d.append(float(x))
    x = round(mean(x_d))
    y_d.append(float(y))
    y = round(mean(y_d))
    z_d.append(float(z))
    z = round(mean(z_d))
return z_v_f, z_v_s, m_xy, c_xy, (y00_f, x0_f), (y11_f, x1_f), x, y, z

我们创建了一个长度为 5 帧的双端队列,并对一行中的所有内容求平均值。

另外,我们计算 y = mx + c,Ax + By + Cz + d = 0,直线的方程是 RGB 中的光线图片和监视器平面的方程,我们得到y = 0。

本文仅做学术分享,如有侵权,请联系删文。

1.面向自动驾驶领域的多传感器数据融合技术2.彻底搞透视觉三维重建:原理剖析、代码讲解、及优化改进3.国内首个面向工业级实战的点云处理课程4.激光-视觉-IMU-GPS融合SLAM算法梳理和代码讲解5.彻底搞懂视觉-惯性SLAM:基于VINS-Fusion正式开课啦6.彻底搞懂基于LOAM框架的3D激光SLAM: 源码剖析到算法优化7.彻底剖析室内、室外激光SLAM关键算法原理、代码和实战(cartographer+LOAM +LIO-SAM)

1. 在「3D视觉工坊」公众号后台回复:即可下载 3D视觉相关资料干货,涉及相机标定、三维重建、立体视觉、SLAM、深度学习、点云后处理、多视图几何等方向。

2. 在「3D视觉工坊」公众号后台回复:即可下载包括等。

3. 在「3D视觉工坊」公众号后台回复:即可下载独家学习课件与视频网址;后台回复:即可下载独家学习课件与视频网址。

扫码添加小助手微信,可

也可申请加入我们的细分方向交流群,目前主要有等微信群。

一定要备注:,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,可快速被通过且邀请进群。也请联系。

▲长按加微信群或投稿

▲长按关注公众号

:针对3D视觉领域的五个方面进行深耕,更有各类大厂的算法工程人员进行技术指导。与此同时,星球将联合知名企业发布3D视觉相关算法开发岗位以及项目对接信息,打造成集技术与就业为一体的铁杆粉丝聚集区,近2000星球成员为创造更好的AI世界共同进步,知识星球入口:

学习3D视觉核心技术,扫描查看介绍,3天内无条件退款

 圈里有高质量教程资料、答疑解惑、助你高效解决问题

标签: cd22激光传感器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台