视线追踪技术是现阶段的人机交互系统中比较关键的技术之一,在车辆、心理、军事、医学领域中都有着广泛的应用前景。从视线追踪技术产生到现在,研究者先后进行了侵入式与非侵入式方法的研究,由于侵入式对使用者而言比较麻烦,因此近年来很多学者都对非侵入式的视线追踪技术进行深入的研究,并取得了较快的发展,但由于人机交互系统与眼动本身的要求,现有的基于视线追踪的人机交互系统需要解决如下问题:减少对使用者的限制;提高系统的运行效率,达到实时性的要求;提高系统的检测准确度与稳定性。
本项目使用的是不需要用户佩戴任何硬件设备并对人体没有影响的非侵入式方法,进行视线追踪,基于摄像头实现人眼检测,瞳孔中心坐标提取,内眼角坐标提取,眼球空间建模,视线估计建模,眼睛空间与计算机屏幕空间映射函数的建立这几个方面。
1、项目简介
本项目首先使用将二维的加权系数降为一维的,并与中值滤波思想相结合的双边滤波算法预处理摄像头采集的图像,去除可能出现的高斯噪声与椒盐噪声,提高了图像质量,然后对图像进行直方图均衡化提高灰度对比度,增加动态范围,为以后更好地提取特征参数提供前提,接着使用基于Haar特征的Adaboost算法检测人脸与眼睛,根据眼睛图像的灰度分布特点,在处理后的图像上使用改进的混合投影函数粗略定位瞳孔中心,使用检测出的中心点像素值作为阀值对眼睛图像进行二值化处理,将处理后的瞳孔进行圆形拟合,求出精确的瞳孔中心,由于眼睛存在不自觉的微动现象,使得检测出来的瞳孔中心位置发生跳动,因此本文将3帧的瞳孔中心坐标的平均值作为特征参数,然后截取瞳孔中心右方包含眼角的眼睛局部图像,缩小内眼角检测的范围,使用改进的自适应阈值的SUSAN角点检测算法定位内眼角位置,此时视线追踪技术需要的眼睛特征参数就全部提取出来了,然后根据人眼的结构特点,本文提出了一个简单的眼睛三维空间模型,用于对将人眼三维坐标投影到二维坐标时产生的误差进行补偿,最后创建视线空间模型,建立眼睛平面参数与计算机屏幕坐标点之间的映射函数,在此函数中加入误差补偿参数,使得视线在屏幕中的落点更为准确,根据视线凝视此区域范围的时间来判断鼠标进行移动或单击操作。
本文使用最简单的USB摄像头,在Python开发平台上建立以视线方向落点为输入的鼠标操作人机交互系统,实现了对视线方向的估计,实验表明在自然光照的环境下,本系统能够比较准确的估计视线方向,实时的反应使用者眼动的情况,与主流的视线估计系统相比,具有硬件要求低,使用方便的优点。
2、系统适用行业和用途
由于视线分为凝视和扫视,而凝视总是能够体现人们感兴趣的区域,使用视线作为输入的人机交互系统能够帮助人们解决很多问题,方便,灵活,受到国内外相关研究人员的普遍关注。将眼动的规律特征与其他的诸如语言,鼠标等交互手段相结合,就会形成一-种更灵活,更接近人类之间正常交流的方式。视线追踪技术的应用范围可以分为以下几个大的领域:
1、计算机领域的应用
视线交互是由视线来完成用户与计算机之间的交流,使用用户的视线可以代替鼠标的操作,用户视线的落点就是鼠标箭头在计算机屏幕上的坐标,使用视线操作计算机时,通过移动视线的方向可以移动鼠标,完成相应的操作。例如,可以通过移动视线来完成向下滚动页面,完成阅读网页或电子书:通过在一个区域凝视规定时间以上来完成单击或双击操作;在操作界面上创建虛拟键盘,可以完成文字输入的功能;能够帮助老人、残疾人控制家用电器设备,实现间接的人机交互功能;通过对视线轨迹的记录对图像进行修改,能够很好的把握用户的意图;可以根据用户视线落点停留的时间决定是否将此点周围领域凸显出来,这就是视线跟随显示。
2、心理学领域
眼睛视线所收集的信息由神经系统进行处理,人类的视觉行为与感知与他的心理活动相关,是人类的想法的体现。因此研究人类的视线能够帮助人们了解人类的心理状况,例如:轮廓错觉的生理心理学调查,阅读心理学研究,视觉搜索机制研究,都使用了视线追踪技术。
3、工业工程
视线追踪技术可以应用于飞行模拟器中,记录模拟飞行时飞行员的眼睛和头部移动情况,提供了飞行员的注视点分析数据,为飞行员飞行提供数据,进行技术上的改进。视线追踪技术还可以应用于车辆驾驶中,可以帮助检查驾驶人的疲劳状态,不同情况下驾驶人的眼睛状态不同,眼睛运动的情况也不同。
3、系统的技术特点和功能
视线追踪技术按照系统的组成与视线方向检测方法不同,可以分为侵入式与非侵入式两种方式。
侵入式视线追踪系统指的是需要用户佩戴研发者特制的配置有光学系统的头盔等设备,能够实时,精确的对用户的眼睛动作做出反应,对视线落点的检测精度比较高,但是对用户的影响比较大。典型的侵入式视线追踪系统有美国应用科学实验室研发的ASL Model H6系列眼动仪,ASL Mobile Eye眼动追踪系统;加拿大SR Rese arch公司生产的EyeLinkII系统,这个系统对用户的动作要求不是很严格;德国SMI公司研发的iViewXHED系统,该系统操作自动化程度高,快速捕捉视线追踪数据等。
非侵入式视线追踪系统指的是使用者不需要佩戴任何设备,只需要利用一台摄像机来采集2D或3D图像,获取眼部和头部图像数据,视线特征数据,根据这些数据进行建模,估算出视线方向及视线的落点位置。对用户的影响比较小,但是需要建立的模型比较复杂,对视线方向的估计相对于侵入式的方法来说不是很准确。典型的非侵入式视线追踪系统有由美国EyeTechDigitalSystems公司研发的QuickGlance2系列,要求用户的眼睛不能离开摄像机的视野范围;美国SmartEye公司设计的SmartEyeAntiSleep2.0眼动仪,具有驾驶员头部模型自动初始化及视线方向初始化校准等功能。
要研究视线追踪的有关问题,首先需要了解有关眼睛的结构,眼睛运动,视觉注意以及,人眼视觉特性等相关特征,当头部有运动时还需要了解关于头部姿势,头部运动等特征。
早在古希腊时期就有关于眼动的研究,但真正对眼动进行观察和实验以及仪器的使用则是从中世纪开始的。阿拉伯人在中世纪早期就将动物的眼睛解剖开来进行研究分析,确定眼睛结构组成,记录光在眼睛中的折射率,将这些眼睛特性应用到眼动规律中,对视觉进行分析。
在最初时期,测量视线方向的手段比较匮乏,研究人员先后使用了机械、电流、电磁等记录眼睛运动的精确数据。机械记录法需要将辅助设备贴在眼睛上记录眼动数据,例如将薄片贴在眼睛角膜.上与标笔相连测试视线方向;电流记录法将特制电极设备放在眼睛的上下部位,利用电位差测试眼动情况;电磁感应法需要麻醉用户的眼睛,将装有探查线圈的镜片吸附在眼睛上,通上电压后根据电压的视敏度得到视线方向。这三种方法对使用者的眼睛都有影响,并且设备比较复杂,属于侵入式视线追踪方式。随着计算机技术,图像处理技术、机器视觉与模式识别技术的不断发展,视线追踪技术也不断的发展起来,技术手段也不断更新,利用摄像机采集人脸与眼睛,并使用图像处理的方法分析眼动数据的光学方法得到了广泛的应用。近年来研究者将注意力主.要集中到光学方法上,本项目中主要应用瞳孔-角膜反光法实现视线追踪。
瞳孔-角膜反光法:当用户看物体视线进行移动时,眼球也伴随着转动,但是人眼相对于头部有些不动的特征,当光线进入人眼时就在眼球的角膜外表面形成光点,这个光点叫做普尔钦斑,当眼球随着感兴趣的物体进行移动时,普尔钦斑相对于头部固定不动。瞳孔角膜反光法就是利用眼球转动时眼睛的--些不变特性与变动特征之间的函数关系,建立几何模型或映射模型来获取视线方向并取得视线的落点。由于这种方法在进行处理时比较方便并且对使用者不会造成不便,使用的情况比较多,比如利用红外线光源获取普尔钦斑与瞳孔中心形成的矢量建立模型获取视线落点位置,如下图所示:
4、项目效果
使用Python+OpenCV实现实时眼动追踪,不需要高端硬件简单摄像头即可实现,效果图如下所示(项目演示视频)。
5、项目源码
项目主程序如下:
import sys
import cv2
import numpy as np
import process
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.uic import loadUi
from PyQt5.QtGui import QPixmap, QImage
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
loadUi('GUImain.ui', self)
with open("style.css", "r") as css:
self.setStyleSheet(css.read())
self.face_decector, self.eye_detector, self.detector = process.init_cv()
self.startButton.clicked.connect(self.start_webcam)
self.stopButton.clicked.connect(self.stop_webcam)
self.camera_is_running = False
self.previous_right_keypoints = None
self.previous_left_keypoints = None
self.previous_right_blob_area = None
self.previous_left_blob_area = None
def start_webcam(self):
if not self.camera_is_running:
self.capture = cv2.VideoCapture(cv2.CAP_DSHOW) # VideoCapture(0) sometimes drops error #-1072875772
if self.capture is None:
self.capture = cv2.VideoCapture(0)
self.camera_is_running = True
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_frame)
self.timer.start(2)
def stop_webcam(self):
if self.camera_is_running:
self.capture.release()
self.timer.stop()
self.camera_is_running = not self.camera_is_running
def update_frame(self): # logic of the main loop
_, base_image = self.capture.read()
self.display_image(base_image)
processed_image = cv2.cvtColor(base_image, cv2.COLOR_RGB2GRAY)
face_frame, face_frame_gray, left_eye_estimated_position, right_eye_estimated_position, _, _ = process.detect_face(
base_image, processed_image, self.face_decector)
if face_frame is not None:
left_eye_frame, right_eye_frame, left_eye_frame_gray, right_eye_frame_gray = process.detect_eyes(face_frame,
face_frame_gray,
left_eye_estimated_position,
right_eye_estimated_position,
self.eye_detector)
if right_eye_frame is not None:
if self.rightEyeCheckbox.isChecked():
right_eye_threshold = self.rightEyeThreshold.value()
right_keypoints, self.previous_right_keypoints, self.previous_right_blob_area = self.get_keypoints(
right_eye_frame, right_eye_frame_gray, right_eye_threshold,
previous_area=self.previous_right_blob_area,
previous_keypoint=self.previous_right_keypoints)
process.draw_blobs(right_eye_frame, right_keypoints)
right_eye_frame = np.require(right_eye_frame, np.uint8, 'C')
self.display_image(right_eye_frame, window='right')
if left_eye_frame is not None:
if self.leftEyeCheckbox.isChecked():
left_eye_threshold = self.leftEyeThreshold.value()
left_keypoints, self.previous_left_keypoints, self.previous_left_blob_area = self.get_keypoints(
left_eye_frame, left_eye_frame_gray, left_eye_threshold,
previous_area=self.previous_left_blob_area,
previous_keypoint=self.previous_left_keypoints)
process.draw_blobs(left_eye_frame, left_keypoints)
left_eye_frame = np.require(left_eye_frame, np.uint8, 'C')
self.display_image(left_eye_frame, window='left')
if self.pupilsCheckbox.isChecked(): # draws keypoints on pupils on main window
self.display_image(base_image)
def get_keypoints(self, frame, frame_gray, threshold, previous_keypoint, previous_area):
keypoints = process.process_eye(frame_gray, threshold, self.detector,
prevArea=previous_area)
if keypoints:
previous_keypoint = keypoints
previous_area = keypoints[0].size
else:
keypoints = previous_keypoint
return keypoints, previous_keypoint, previous_area
def display_image(self, img, window='main'):
# Makes OpenCV images displayable on PyQT, displays them
qformat = QImage.Format_Indexed8
if len(img.shape) == 3:
if img.shape[2] == 4: # RGBA
qformat = QImage.Format_RGBA8888
else: # RGB
qformat = QImage.Format_RGB888
out_image = QImage(img, img.shape[1], img.shape[0], img.strides[0], qformat) # BGR to RGB
out_image = out_image.rgbSwapped()
if window == 'main': # main window
self.baseImage.setPixmap(QPixmap.fromImage(out_image))
self.baseImage.setScaledContents(True)
if window == 'left': # left eye window
self.leftEyeBox.setPixmap(QPixmap.fromImage(out_image))
self.leftEyeBox.setScaledContents(True)
if window == 'right': # right eye window
self.rightEyeBox.setPixmap(QPixmap.fromImage(out_image))
self.rightEyeBox.setScaledContents(True)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.setWindowTitle("GUI")
window.show()
sys.exit(app.exec_())
人眼检测程序如下:
import os
import cv2
import numpy as np
def init_cv():
"""loads all of cv2 tools"""
face_detector = cv2.CascadeClassifier(
os.path.join("Classifiers", "haar", "haarcascade_frontalface_default.xml"))
eye_detector = cv2.CascadeClassifier(os.path.join("Classifiers", "haar", 'haarcascade_eye.xml'))
detector_params = cv2.SimpleBlobDetector_Params()
detector_params.filterByArea = True
detector_params.maxArea = 1500
detector = cv2.SimpleBlobDetector_create(detector_params)
return face_detector, eye_detector, detector
def detect_face(img, img_gray, cascade):
"""
Detects all faces, if multiple found, works with the biggest. Returns the following parameters:
1. The face frame
2. A gray version of the face frame
2. Estimated left eye coordinates range
3. Estimated right eye coordinates range
5. X of the face frame
6. Y of the face frame
"""
coords = cascade.detectMultiScale(img, 1.3, 5)
if len(coords) > 1:
biggest = (0, 0, 0, 0)
for i in coords:
if i[3] > biggest[3]:
biggest = i
biggest = np.array([i], np.int32)
elif len(coords) == 1:
biggest = coords
else:
return None, None, None, None, None, None
for (x, y, w, h) in biggest:
frame = img[y:y + h, x:x + w]
frame_gray = img_gray[y:y + h, x:x + w]
lest = (int(w * 0.1), int(w * 0.45))
rest = (int(w * 0.55), int(w * 0.9))
X = x
Y = y
return frame, frame_gray, lest, rest, X, Y
def detect_eyes(img, img_gray, lest, rest, cascade):
"""
:param img: image frame
:param img_gray: gray image frame
:param lest: left eye estimated position, needed to filter out nostril, know what eye is found
:param rest: right eye estimated position
:param cascade: Hhaar cascade
:return: colored and grayscale versions of eye frames
"""
leftEye = None
rightEye = None
leftEyeG = None
rightEyeG = None
coords = cascade.detectMultiScale(img_gray, 1.3, 5)
if coords is None or len(coords) == 0:
pass
else:
for (x, y, w, h) in coords:
eyecenter = int(float(x) + (float(w) / float(2)))
if lest[0] < eyecenter and eyecenter < lest[1]:
leftEye = img[y:y + h, x:x + w]
leftEyeG = img_gray[y:y + h, x:x + w]
leftEye, leftEyeG = cut_eyebrows(leftEye, leftEyeG)
elif rest[0] < eyecenter and eyecenter < rest[1]:
rightEye = img[y:y + h, x:x + w]
rightEyeG = img_gray[y:y + h, x:x + w]
rightEye, rightEye = cut_eyebrows(rightEye, rightEyeG)
else:
pass # nostril
return leftEye, rightEye, leftEyeG, rightEyeG
def process_eye(img, threshold, detector, prevArea=None):
"""
:param img: eye frame
:param threshold: threshold value for threshold function
:param detector: blob detector
:param prevArea: area of the previous keypoint(used for filtering)
:return: keypoints
"""
_, img = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)
img = cv2.erode(img, None, iterations=2)
img = cv2.dilate(img, None, iterations=4)
img = cv2.medianBlur(img, 5)
keypoints = detector.detect(img)
if keypoints and prevArea and len(keypoints) > 1:
tmp = 1000
for keypoint in keypoints: # filter out odd blobs
if abs(keypoint.size - prevArea) < tmp:
ans = keypoint
tmp = abs(keypoint.size - prevArea)
keypoints = np.array(ans)
return keypoints
def cut_eyebrows(img, imgG):
height, width = img.shape[:2]
img = img[15:height, 0:width] # cut eyebrows out (15 px)
imgG = imgG[15:height, 0:width]
return img, imgG
def draw_blobs(img, keypoints):
"""Draws blobs"""
cv2.drawKeypoints(img, keypoints, img, (0, 0, 255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)