让我们看看下面的情况,你坐在图书馆里,你刚刚看到最漂亮的女人坐在图书馆的另一边。哎呀,她发现你在盯着她看。她估计你的目光在盯着她,而你通过理解她的目光指向你,注意到被她抓个正着。
眼睛凝视:一个人的眼睛聚焦的点
就像我们惊人的大脑毫不费力地完成许多任务一样,这是一个很难“教”计算机的问题,因为我们需要执行几项艰巨的任务:
人脸识别
眼睛识别和瞳孔定位
确定头部和眼睛的 3D 定位
商业凝视跟踪器有各种形状和大小。从眼镜到屏幕的基础解决方案。但是,尽管这些产品精度很高,但它们使用的是专有软件和硬件,而且非常昂贵。
为了使这篇博客的篇幅保持合理,我们将构建一个基本的注视跟踪形式。有几个粗略的估计。而且我们不会确定确切的注视点,而是确定注视方向。
凝视是相对于镜头的,而我坐在镜头下人脸识别和瞳孔定位
对于这项任务,我们将使用MediaPipe(https://google.github.io/mediapipe/solutions/face_mesh.html),这是一个由 Google 开发的惊人的深度学习框架,它将实时为我们提供 468 个 2D 人脸地标,而使用很少的资源。
让我们看一些代码:
- import mediapipe as mp
- import cv2
- import gaze
-
- mp_face_mesh = mp.solutions.face_mesh # initialize the face mesh model
-
- # camera stream:
- cap = cv2.VideoCapture(1)
- with mp_face_mesh.FaceMesh(
- max_num_faces=1, # number of faces to track in each frame
- refine_landmarks=True, # includes iris landmarks in the face mesh model
- min_detection_confidence=0.5,
- min_tracking_confidence=0.5) as face_mesh:
- while cap.isOpened():
- success, image = cap.read()
- if not success: # no frame input
- print("Ignoring empty camera frame.")
- continue
- # To improve performance, optionally mark the image as not writeable to
- # pass by reference.
- image.flags.writeable = False
- image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # frame to RGB for the face-mesh model
- results = face_mesh.process(image)
- image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
-
- if results.multi_face_landmarks:
- gaze.gaze(image, results.multi_face_landmarks[0])
-
- cv2.imshow('output window', image)
- if cv2.waitKey(2) & 0xFF == 27:
- break
- cap.release()
这里没什么特别的,在第 27 行,我们将从 mediapipe 框架获得的当前帧和面部标志点传递给我们的 gaze 函数,这就是所有乐趣所在。
视线追踪是一个 3D 问题,但我们在标题中说我们只使用了一个简单的网络摄像头,这怎么可能呢?
我们将使用一些魔法(线性代数)来实现它。
首先,让我们了解一下我们的相机是如何“看到”这个世界的。
来自 OpenCV 文档的图像
看屏幕时看到的 2D 图像用蓝色表示,3D 世界用世界坐标系表示。它们之间有什么联系?我们如何从 2D 图像映射 3D 世界,或者至少得到一个粗略的估计?
让我们弄清楚吧!
我们人类比我们想象的更相似,我们可以采用人脸的通用 3D 模型,这将是对大多数人口的 3D 比例的一个很好的估计。
让我们使用这样的模型来定义一个 3D 坐标系,我们将鼻尖设置为我们的坐标系的原点,相对于它我们将再定义 5 个点,如下所示:
- def gaze(frame, points):
- '''
- 2D image points.
- relative takes mediapipe points that normelized to [-1, 1] and returns image points
- at (x,y) format
- '''
- image_points = np.array([
- relative(points.landmark[4], frame.shape), # Nose tip
- relative(points.landmark[152], frame.shape), # Chin
- relative(points.landmark[263], frame.shape), # Left eye left corner
- relative(points.landmark[33], frame.shape), # Right eye right corner
- relative(points.landmark[287], frame.shape), # Left Mouth corner
- relative(points.landmark[57], frame.shape) # Right mouth corner
- ], dtype="double")
-
- # 3D model points.
- model_points = np.array([
- (0.0, 0.0, 0.0), # Nose tip
- (0, -63.6, -12.5), # Chin
- (-43.3, 32.7, -26), # Left eye left corner
- (43.3, 32.7, -26), # Right eye right corner
- (-28.9, -28.9, -24.1), # Left Mouth corner
- (28.9, -28.9, -24.1) # Right mouth corner
- ])
- '''
- 3D model eye points
- The center of the eye ball
- '''
- Eye_ball_center_right = np.array([[-29.05],[32.7],[-39.5]])
- Eye_ball_center_left = np.array([[29.05],[32.7],[-39.5]])
现在我们有 6 个从 mediapipe 获得的 2D 点,以及我们定义的世界坐标系中的相应 3D 点。我们的目标是了解这些点的 3D 位置的变化,并通过使用我们的 2D 图像来做到这一点。我们该怎么做?
针孔相机模型是一种数学模型,它描述了 3D 世界中的点之间的关系以及它们在 2D 图像平面上的投影。从这个模型中,我们将得出以下方程:
使用这个等式,我们可以获得将 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
- '''
- camera matrix estimation
- '''
- focal_length = frame.shape[1]
- center = (frame.shape[1] / 2, frame.shape[0] / 2)
- camera_matrix = np.array(
- [[focal_length, 0, center[0]],
- [0, focal_length, center[1]],
- [0, 0, 1]], dtype="double"
- )
-
- dist_coeffs = np.zeros((4, 1)) # Assuming no lens distortion
- (success, rotation_vector, translation_vector) = cv2.solvePnP(model_points, image_points, camera_matrix,
- dist_coeffs, flags=cv2.cv2.SOLVEPNP_ITERATIVE)
使用我们的新转换,我们可以从 3D 空间中取出一个点并将其投影到 2D 图像平面。因此,我们将了解这个 3D 点在空间中指向的位置。这就是点 (0,0,150) 的样子。
现在我们将获取瞳孔 2D 图像坐标并将它们投影到我们的 3D 模型坐标。与我们在头部姿势估计部分所做的正好相反。
- # project image point to world point
- _ ,transformation, _ = cv2.estimateAffine3D(image_points1, model_points) # image cord to world cord tramsformation
- 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 模型点。
注意:这不是一个非常准确的估计
我没有告诉你,但是如果你看上面的第二个代码片段,你可以看到我们有眼睛中心模型点(3D),我们刚刚使用 estimateAffline3D 获取了瞳孔3D模型点。
现在要找到注视方向,我们需要解决这个线平面相交问题,如上图所述。我们试图找到的点用 S 表示。让我们将点投影到 2D 平面中。
- # project pupil image point into world point
- pupil_world_cord = transformation @ np.array([[left_pupil[0],left_pupil[1],0,1]]).T
-
- # 3D gaze point (10 is arbitrary value denoting gaze distance)
- S = Eye_ball_center_left + (pupil_world_cord - Eye_ball_center_left) * 10
-
- # Project a 3D gaze point onto the image plane.
- (eye_pupil2D, jacobian) = cv2.projectPoints((int(S[0]), int(S[1]), int(S[2])), rotation_vector,
- translation_vector, camera_matrix, dist_coeffs)
- # Draw gaze line into screen
- p1 = (int(left_pupil[0]), int(left_pupil[1]))
- p2 = (int(eye_pupil2D[0][0][0]) , int(eye_pupil2D[0][0][1]))
- cv2.line(frame, p1, p2, (0, 0, 255), 2)
注意:在第 5 行中,我们使用“魔术”数 10,这是因为我们不知道拍摄对象与相机的距离。所以图中用 t 表示的瞳孔到相机的距离是未知的
还没有。现在我们需要考虑头部运动,这样,们的视线追踪器就能适应头部的运动。让我们从一开始就使用我们的头部姿势估计。
瞳孔的 2D 位置由点 p 表示,点 g 是注视 + 头部旋转投影,点h是头部姿势投影。现在为了获得干净的注视信息,我们从向量A中构造向量B。
- # Project a 3D gaze direction onto the image plane.
- (eye_pupil2D, _) = cv2.projectPoints((int(S[0]), int(S[1]), int(S[2])), rotation_vector,
- translation_vector, camera_matrix, dist_coeffs)
- # project 3D head pose into the image plane
- (head_pose, _) = cv2.projectPoints((int(pupil_world_cord[0]), int(pupil_world_cord[1]), int(40)), rotation_vector,
- translation_vector, camera_matrix, dist_coeffs)
-
- # correct gaze for head rotation
- 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
我们可以改变一些东西来提高准确性:
正确校准相机,不要使用估计。
使用双眼,计算两个位置之间的平均值。(我们只用了左眼)
我们正在使用estimateAffine3D 方法将2d 瞳孔位置投影到3d 空间中,但这不是一个准确的估计。我们可以使用眼睛结构和眼窝中的瞳孔位置来推断瞳孔的 3d 位置。
我们完全忽略了拍摄对象与相机的距离。正因为如此,我们只得到了一个注视方向而不是一个注视点。它可能是最重要的部分,但也是最复杂的部分。
通过一些工作,你可以实施你的解决方案,并将其用于你的特定需求。
☆ END ☆
如果看到这里,说明你喜欢这篇文章,请转发、点赞。微信搜索「uncle_pn」,欢迎添加小编微信「 woshicver」,每日朋友圈更新一篇高质量博文。
↓扫描二维码添加小编↓