• 使用网络摄像头进行眼睛注视估计


    让我们看看下面的情况,你坐在图书馆里,你刚刚看到最漂亮的女人坐在图书馆的另一边。哎呀,她发现你在盯着她看。她估计你的目光在盯着她,而你通过理解她的目光指向你,注意到被她抓个正着。

    眼睛凝视:一个人的眼睛聚焦的点

    就像我们惊人的大脑毫不费力地完成许多任务一样,这是一个很难“教”计算机的问题,因为我们需要执行几项艰巨的任务:

    • 人脸识别

    • 眼睛识别和瞳孔定位

    • 确定头部和眼睛的 3D 定位

    商业凝视跟踪器有各种形状和大小。从眼镜到屏幕的基础解决方案。但是,尽管这些产品精度很高,但它们使用的是专有软件和硬件,而且非常昂贵。

    让我们开始构建我们的视线跟踪器

    为了使这篇博客的篇幅保持合理,我们将构建一个基本的注视跟踪形式。有几个粗略的估计。而且我们不会确定确切的注视点,而是确定注视方向。

    5ef6958f124dc0c180678e163cbfcbb8.gif

    凝视是相对于镜头的,而我坐在镜头下

    人脸识别和瞳孔定位

    对于这项任务,我们将使用MediaPipe(https://google.github.io/mediapipe/solutions/face_mesh.html),这是一个由 Google 开发的惊人的深度学习框架,它将实时为我们提供 468 个 2D 人脸地标,而使用很少的资源。

    让我们看一些代码:

    1. import mediapipe as mp
    2. import cv2
    3. import gaze
    4. mp_face_mesh = mp.solutions.face_mesh # initialize the face mesh model
    5. # camera stream:
    6. cap = cv2.VideoCapture(1)
    7. with mp_face_mesh.FaceMesh(
    8.         max_num_faces=1,                            # number of faces to track in each frame
    9.         refine_landmarks=True,                      # includes iris landmarks in the face mesh model
    10.         min_detection_confidence=0.5,
    11.         min_tracking_confidence=0.5) as face_mesh:
    12.     while cap.isOpened():
    13.         success, image = cap.read()
    14.         if not success:                            # no frame input
    15.             print("Ignoring empty camera frame.")
    16.             continue
    17.         # To improve performance, optionally mark the image as not writeable to
    18.         # pass by reference.
    19.         image.flags.writeable = False
    20.         image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # frame to RGB for the face-mesh model
    21.         results = face_mesh.process(image)
    22.         image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
    23.         if results.multi_face_landmarks:
    24.             gaze.gaze(image, results.multi_face_landmarks[0])
    25.         cv2.imshow('output window', image)
    26.         if cv2.waitKey(2) & 0xFF == 27:          
    27.             break
    28. cap.release()

    这里没什么特别的,在第 27 行,我们将从 mediapipe 框架获得的当前帧和面部标志点传递给我们的 gaze  函数,这就是所有乐趣所在。

    2D转3D?

    视线追踪是一个 3D 问题,但我们在标题中说我们只使用了一个简单的网络摄像头,这怎么可能呢?

    我们将使用一些魔法(线性代数)来实现它。

    首先,让我们了解一下我们的相机是如何“看到”这个世界的。

    d0b618e5ad308350abf838dc05efdce4.png

    来自 OpenCV 文档的图像

    看屏幕时看到的 2D 图像用蓝色表示,3D 世界用世界坐标系表示。它们之间有什么联系?我们如何从 2D 图像映射 3D 世界,或者至少得到一个粗略的估计?

    让我们弄清楚吧!

    我们都一样

    我们人类比我们想象的更相似,我们可以采用人脸的通用 3D 模型,这将是对大多数人口的 3D 比例的一个很好的估计。

    让我们使用这样的模型来定义一个 3D 坐标系,我们将鼻尖设置为我们的坐标系的原点,相对于它我们将再定义 5 个点,如下所示:

    1. def gaze(frame, points):
    2.     '''
    3.     2D image points.
    4.     relative takes mediapipe points that normelized to [-1, 1] and returns image points
    5.     at (x,y) format
    6.     '''
    7.     image_points = np.array([
    8.         relative(points.landmark[4], frame.shape),    # Nose tip
    9.         relative(points.landmark[152], frame.shape),  # Chin
    10.         relative(points.landmark[263], frame.shape),  # Left eye left corner
    11.         relative(points.landmark[33], frame.shape),   # Right eye right corner
    12.         relative(points.landmark[287], frame.shape),  # Left Mouth corner
    13.         relative(points.landmark[57], frame.shape)    # Right mouth corner
    14.     ], dtype="double")
    15.     # 3D model points.
    16.     model_points = np.array([
    17.         (0.00.00.0),       # Nose tip
    18.         (0-63.6-12.5),     # Chin
    19.         (-43.332.7-26),    # Left eye left corner
    20.         (43.332.7-26),     # Right eye right corner
    21.         (-28.9-28.9-24.1), # Left Mouth corner
    22.         (28.9-28.9-24.1)   # Right mouth corner
    23.     ])
    24.     '''
    25.     3D model eye points
    26.     The center of the eye ball
    27.     '''
    28.     Eye_ball_center_right = np.array([[-29.05],[32.7],[-39.5]])
    29.     Eye_ball_center_left = np.array([[29.05],[32.7],[-39.5]])

    现在我们有 6 个从 mediapipe 获得的 2D 点,以及我们定义的世界坐标系中的相应 3D 点。我们的目标是了解这些点的 3D 位置的变化,并通过使用我们的 2D 图像来做到这一点。我们该怎么做?

    针孔相机模型救援

    针孔相机模型是一种数学模型,它描述了 3D 世界中的点之间的关系以及它们在 2D 图像平面上的投影。从这个模型中,我们将得出以下方程:

    ce92fa42bb308cca58e991752ce36ff8.png

    使用这个等式,我们可以获得将 3D 点投影到图像 2D 图像平面的变换。但是我们能解决吗?好吧,至少不是通过简单的代数工具,但你不用担心,这就是 OpenCV 使用 solvePnP 函数的地方,请查看链接以获得更深入的解释:

    https://docs.opencv.org/4.5.4/d9/d0c/group__calib3d.html#ga549c2075fac14829ff4a58bc931c033d

    我们将获取我们的 6 个图像点和相应的 3D 模型点,并将它们传递给 solvepnp 函数。作为回报,我们将获得一个旋转和平移向量,从而得到一个变换,这将帮助我们将一个点从 3D 世界点投影到 2D 平面。

    在这里学习如何估计相机矩阵:

    https://learnopencv.com/approximate-focal-length-for-webcams-and-cell-phone-cameras/

    或者在这里学习如何校准你自己的相机:

    https://docs.opencv.org/3.4/dc/dbb/tutorial_py_calibration.html

    1. '''
    2.     camera matrix estimation
    3.     '''
    4.     focal_length = frame.shape[1]
    5.     center = (frame.shape[1] / 2, frame.shape[0] / 2)
    6.     camera_matrix = np.array(
    7.         [[focal_length, 0, center[0]],
    8.          [0, focal_length, center[1]],
    9.          [001]], dtype="double"
    10.     )
    11.     dist_coeffs = np.zeros((41))  # Assuming no lens distortion
    12.     (success, rotation_vector, translation_vector) = cv2.solvePnP(model_points, image_points, camera_matrix,
    13.                                                                   dist_coeffs, flags=cv2.cv2.SOLVEPNP_ITERATIVE)

    使用我们的新转换,我们可以从 3D 空间中取出一个点并将其投影到 2D 图像平面。因此,我们将了解这个 3D 点在空间中指向的位置。这就是点 (0,0,150) 的样子。

    7099b8f4acee16ba0256c271d766fddd.gif

    2D从3D

    现在我们将获取瞳孔 2D 图像坐标并将它们投影到我们的 3D 模型坐标。与我们在头部姿势估计部分所做的正好相反。

    1. # project image point to world point
    2. _ ,transformation, _ = cv2.estimateAffine3D(image_points1, model_points) # image cord to world cord tramsformation
    3. pupil_world_cord =  transformation @ np.array([[left_pupil[0],left_pupil[1],0,1]]).T # Transformation * pupil image point vector

    如代码片段所示,我们将使用 OpenCV 的估计 Affline3D 函数。此函数使用我们讨论的针孔相机模型的相同原理。它采用两组 3D 点并返回第一组和第二组之间的转换。但是等等,我们的图像点是二维的,这怎么可能?

    好吧,我们将获取图像点 (x,y) 并将它们作为 (x,y,0) 传递,因此将获得图像坐标到模型坐标之间的转换。使用这种方法,我们可以从我们从 mediapipe 获取的 2D 图像点获取瞳孔 3D 模型点。

    注意:这不是一个非常准确的估计

    7594a0a5f7fd148da69161f91fd8353d.png

    我没有告诉你,但是如果你看上面的第二个代码片段,你可以看到我们有眼睛中心模型点(3D),我们刚刚使用 estimateAffline3D 获取了瞳孔3D模型点。

    现在要找到注视方向,我们需要解决这个线平面相交问题,如上图所述。我们试图找到的点用 S 表示。让我们将点投影到 2D 平面中。

    1. # project pupil image point into world point 
    2.     pupil_world_cord =  transformation @ np.array([[left_pupil[0],left_pupil[1],0,1]]).T
    3.     
    4.     # 3D gaze point (10 is arbitrary value denoting gaze distance)
    5.     S = Eye_ball_center_left + (pupil_world_cord - Eye_ball_center_left) * 10
    6.     
    7.     # Project a 3D gaze point onto the image plane.
    8.     (eye_pupil2D, jacobian) = cv2.projectPoints((int(S[0]), int(S[1]), int(S[2])), rotation_vector,
    9.                                                     translation_vector, camera_matrix, dist_coeffs)
    10.     # Draw gaze line into screen 
    11.     p1  = (int(left_pupil[0]), int(left_pupil[1]))
    12.     p2 = (int(eye_pupil2D[0][0][0]) , int(eye_pupil2D[0][0][1]))
    13.     cv2.line(frame, p1, p2, (00255), 2)

    注意:在第 5 行中,我们使用“魔术”数 10,这是因为我们不知道拍摄对象与相机的距离。所以图中用 t 表示的瞳孔到相机的距离是未知的

    完了吗?

    还没有。现在我们需要考虑头部运动,这样,们的视线追踪器就能适应头部的运动。让我们从一开始就使用我们的头部姿势估计。

    72c94ef5f1bb6eee1b9961d101175def.png

    瞳孔的 2D 位置由点 p 表示,点 g 是注视 + 头部旋转投影,点h是头部姿势投影。现在为了获得干净的注视信息,我们从向量A中构造向量B。

    1. # Project a 3D gaze direction onto the image plane.
    2. (eye_pupil2D, _) = cv2.projectPoints((int(S[0]), int(S[1]), int(S[2])), rotation_vector,
    3.                                                 translation_vector, camera_matrix, dist_coeffs)
    4. # project 3D head pose into the image plane
    5. (head_pose, _) = cv2.projectPoints((int(pupil_world_cord[0]), int(pupil_world_cord[1]), int(40)), rotation_vector,
    6.                                                 translation_vector, camera_matrix, dist_coeffs)
    7. # correct gaze for head rotation
    8. gaze = left_pupil + (eye_pupil2D[0][0] - left_pupil) - (head_pose[0][0] - left_pupil)

    在第 5 行中,我们使用了“魔术”数 40,原因与我们在上面的代码片段中使用 10 的原因相同。

    结束

    我们已经完成了,至少现在是这样。你可以在 Github 页面上看到完整的代码,并在你的机器上运行它:

    https://github.com/amitt1236/Gaze_estimation

    但是我们真的完成了吗?

    我们可以改变一些东西来提高准确性:

    1. 正确校准相机,不要使用估计。

    2. 使用双眼,计算两个位置之间的平均值。(我们只用了左眼)

    3. 我们正在使用estimateAffine3D 方法将2d 瞳孔位置投影到3d 空间中,但这不是一个准确的估计。我们可以使用眼睛结构和眼窝中的瞳孔位置来推断瞳孔的 3d 位置。

    4. 我们完全忽略了拍摄对象与相机的距离。正因为如此,我们只得到了一个注视方向而不是一个注视点。它可能是最重要的部分,但也是最复杂的部分。

    通过一些工作,你可以实施你的解决方案,并将其用于你的特定需求。

    ☆ END ☆

    如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 woshicver」,每日朋友圈更新一篇高质量博文。

    扫描二维码添加小编↓

    25ba7df080821dc62165aa883b3a64ef.png

  • 相关阅读:
    excel排序没有 扩展选定区域
    C++11
    【云原生】Docker镜像详解
    SaaS企业如何逐步进化与组织适配的销售力?
    【指针详解】(上)看一遍就会❗❗❗家人们冲❗
    【K8S专栏】Kubernetes有状态应用管理
    《视觉 SLAM 十四讲》V2 第 9 讲 后端优化1 【扩展卡尔曼滤波器 EKF && BA+非线性优化(Ceres、g2o)】
    Golang 实现 Redis(11): RDB 文件格式
    缺失的第一个正整数
    【滤波跟踪】基于UKF与KF实现单目标无杂波环境下二维雷达量测的目标跟踪算法附matlab代码
  • 原文地址:https://blog.csdn.net/woshicver/article/details/125437634