简介:本资源是一套面向心理学研究者、人机交互工程师及UI/UX设计师的Python眼动数据分析工具包,聚焦注视点轨迹绘制与热图生成两大核心任务,解决眼动数据难以直观呈现、行为模式难量化的问题。压缩包共5个文件(3个核心Python模块、1份说明文档和1个依赖清单),总大小仅17KB,轻量易部署:gaze_visualizer.py负责多维可视化(含动态热图与3D轨迹)、eyetracking_analyzer.py实现注视检测、AOI分析与扫视识别,main.py提供开箱即用的分析流程。已有126人学习下载,适合具备基础Python能力的研究人员快速开展认知负荷评估、界面热点诊断或跨被试视觉行为对比。代码采用模块化设计,算法严格遵循眼动研究规范,支持高采样率数据实时处理,并内置数据清洗、质量评估与自适应热图优化功能,可直接嵌入实验分析 pipeline。
1. 项目概述:从数据到洞察,眼动追踪可视化的核心价值
眼动追踪技术,早已不再是实验室里的昂贵玩具。从用户体验研究、广告效果评估,到心理学实验、驾驶行为分析,甚至是游戏和VR/AR领域的交互优化,我们正处在一个需要量化“视觉注意力”的时代。作为一名长期混迹于数据科学和交互设计交叉领域的老兵,我处理过大量眼动数据。最深刻的体会是:原始的眼动坐标数据只是一堆冰冷的数字,而真正产生价值的,是将这些数据转化为直观、可解释的视觉故事。这就是“眼动追踪数据可视化”项目的核心——它是一座桥梁,连接着生硬的采样点序列和人类可理解的视觉行为模式。
这个项目具体要做什么?简单说,就是给你一套眼动仪(比如Tobii、EyeLink等)采集到的原始数据,通常包含时间戳、X/Y坐标、瞳孔直径等,然后通过Python编程,将其转化为两种最经典、最有力的可视化形式:注视点轨迹图和注意力热图。轨迹图能清晰展示视觉扫描路径,回答“眼睛看了哪里,按什么顺序看的”;热图则通过颜色密度直观呈现视觉注意力的空间分布,回答“哪里看得最多、最久”。对于产品经理、交互设计师、市场研究员或心理学学生来说,掌握这套从数据处理到成图的分析流水线,意味着你能独立、快速地从海量眼动数据中挖掘出关键洞察,而无需依赖昂贵或封闭的商用分析软件。
本文将手把手带你用Python搭建一个完整的、可复现的眼动数据可视化分析流程。我会从最原始的数据清洗讲起,涵盖轨迹绘制、热图生成的所有核心算法与细节参数,并分享我踩过的无数个坑和总结出的实战技巧。无论你是刚接触Python的数据分析新手,还是想将眼动分析能力工具化的资深从业者,都能从这里找到可直接“抄作业”的代码和思路。
2. 核心思路与方案选型:为什么是Python + Matplotlib/Seaborn?
在动手写代码之前,我们先要理清技术选型背后的逻辑。眼动可视化不是简单的画图,它涉及时序数据处理、空间坐标映射、统计聚合以及美学渲染。市面上有专业的眼动分析软件(如Tobii Pro Lab, iMotions),它们功能强大但价格不菲,且脚本化和批处理能力往往受限。而Python生态,以其在科学计算(NumPy, SciPy)、数据分析(Pandas)和可视化(Matplotlib, Seaborn, Plotly)领域的绝对优势,成为了构建灵活、自动化分析管道的理想选择。
2.1 数据处理基石:Pandas 与 NumPy
眼动数据通常是CSV或TXT格式,包含数万甚至数百万行记录。Pandas的DataFrame是处理这种表格数据的利器,其强大的数据筛选、分组、聚合和清洗功能(如处理无效数据、插值)是后续所有分析的基础。NumPy则为我们提供了高效的数组运算能力,特别是在计算注视点停留时间、进行空间网格化统计(为热图准备)时,向量化操作能带来数量级的性能提升。
2.2 可视化引擎的选择:Matplotlib 与 Seaborn 的黄金组合
这是本项目的可视化核心。我选择Matplotlib作为底层绘图引擎,原因在于其无与伦比的灵活性和可控性。绘制注视点轨迹时,我们需要精确控制每个点的位置、大小(代表注视时长)、连线的样式和颜色(代表时间顺序或被试)。Matplotlib的scatter和plot函数配合事件循环,可以完美实现这些定制化需求。
而热图的生成,我强烈推荐在Matplotlib基础上使用Seaborn的heatmap函数,或者直接使用Matplotlib的imshow。Seaborn的heatmap在统计聚合数据的可视化上更加便捷,配色科学,且能轻松添加数值标注。但需要注意的是,我们需要先将二维空间中的注视点坐标,通过二维核密度估计(KDE)或简单的直方图统计,转换为一个二维的密度矩阵,这个矩阵才是heatmap或imshow的输入。这里我会详细介绍两种热图生成方法的优劣和适用场景。
2.3 为什么不选Plotly或更高级的库?
Plotly能生成交互式图表,对于在线报告非常友好。但在初期构建分析原型和进行大批量、自动化报告生成时,Matplotlib的静态图片输出更稳定、更快速,且对系统资源要求更低。我们的首要目标是实现可靠、可复现的分析流程,交互性可以作为后续的增强功能。因此,本方案以Matplotlib/Seaborn为核心,确保代码在任何Python环境中都能稳定运行。
注意:确保你的Python环境已安装上述库。可以通过
pip install numpy pandas matplotlib seaborn scipy一键安装。SciPy库将用于可选的核密度估计计算。
3. 数据预处理:清洗与结构化,一切分析的起点
拿到眼动数据,第一步永远不是画图,而是“看”数据和“洗”数据。原始数据往往充满噪声和缺失值,直接可视化只会得到一幅充满误导性的画面。
3.1 理解你的数据格式
一个典型的眼动数据文件(CSV)可能包含以下列:
timestamp: 采样时间点(毫秒或微秒)。gaze_point_x,gaze_point_y: 注视点在屏幕或刺激材料上的坐标(通常以像素为单位)。这是我们的核心数据。pupil_diameter: 瞳孔直径,可用于认知负荷等衍生分析,本项目暂不涉及。validity: 数据有效性标志(如0=有效,1=无效)。这是清洗的关键。stimulus: 当前呈现的刺激物名称,用于分段分析。
首先,用Pandas加载数据并查看基本信息:
import pandas as pd import numpy as np # 加载数据,假设分隔符是逗号 df = pd.read_csv('eyetracking_data.csv') print(df.head()) # 查看前几行 print(df.info()) # 查看列信息和缺失值 print(df['validity'].value_counts()) # 查看有效性标签分布3.2 关键清洗步骤
- 处理无效数据:根据
validity列,过滤掉标记为无效的数据点。# 假设 validity 为0表示有效 df_clean = df[df['validity'] == 0].copy() - 处理坐标异常值:眼动仪可能因眨眼、头部移动产生坐标值为NaN或超出屏幕范围(如负数或大于屏幕分辨率)的异常点。
screen_width, screen_height = 1920, 1080 # 根据你的实验设置修改 # 剔除超出屏幕范围的注视点 df_clean = df_clean[ (df_clean['gaze_point_x'].between(0, screen_width)) & (df_clean['gaze_point_y'].between(0, screen_height)) ] # 填充或删除剩余的NaN值(谨慎操作,填充可能引入偏差) df_clean['gaze_point_x'].fillna(method='ffill', inplace=True) # 前向填充 df_clean['gaze_point_y'].fillna(method='ffill', inplace=True) # 或者直接删除:df_clean.dropna(subset=['gaze_point_x', 'gaze_point_y'], inplace=True) - 时间戳处理与采样率检查:确保时间戳是单调递增的。计算实际采样率,与设备标称值对比,可发现数据记录是否完整。
df_clean['timestamp'] = pd.to_numeric(df_clean['timestamp'], errors='coerce') df_clean.sort_values('timestamp', inplace=True) # 计算采样间隔和平均采样率 time_intervals = np.diff(df_clean['timestamp']) avg_sample_rate = 1 / (np.mean(time_intervals) / 1000.0) # 假设时间戳单位是毫秒,转换为Hz print(f"平均采样率: {avg_sample_rate:.2f} Hz")
3.3 定义注视点(Fixation)
原始数据是连续的“注视点采样”,但我们需要识别出真正的“注视事件”(即视线在某个小区域内保持相对稳定一段时间)。这里涉及一个核心算法:速度-阈值识别算法(I-VT)。简单来说,就是计算连续采样点之间的角速度(或像素位移速度),将低于某个阈值的连续点聚类为一个注视点。
对于入门或快速分析,我们可以采用一种简化方法:基于时间的滑动窗口。例如,将连续100毫秒内,坐标标准差小于某个阈值(如20像素)的采样点聚合为一个注视点,用这些点的坐标均值作为注视点位置,持续时间作为注视时长。
def identify_fixations_simple(df, time_window=100, std_threshold=20): """ 简化版注视点识别(基于时间和空间稳定性)。 注意:这是一个启发式方法,对于精确研究,请使用成熟的算法库(如PyGazeAnalyser)。 """ fixations = [] start_idx = 0 while start_idx < len(df): # 获取时间窗口内的数据 start_time = df.iloc[start_idx]['timestamp'] window_df = df[(df['timestamp'] >= start_time) & (df['timestamp'] < start_time + time_window)] if len(window_df) < 2: start_idx += 1 continue # 计算窗口内坐标的标准差 x_std = window_df['gaze_point_x'].std() y_std = window_df['gaze_point_y'].std() if x_std < std_threshold and y_std < std_threshold: # 符合注视点条件,计算平均位置和持续时间 fixation_x = window_df['gaze_point_x'].mean() fixation_y = window_df['gaze_point_y'].mean() duration = window_df['timestamp'].max() - window_df['timestamp'].min() fixations.append({'x': fixation_x, 'y': fixation_y, 'duration': duration}) # 跳过这个窗口,从下一个点开始 start_idx += len(window_df) else: start_idx += 1 # 不符合,移动一个采样点 return pd.DataFrame(fixations) # 应用简化识别算法 fixation_df = identify_fixations_simple(df_clean) print(f"识别出 {len(fixation_df)} 个注视点")实操心得:简化算法适用于数据质量较高、对精度要求不极端的场景。对于学术研究或商业报告,强烈建议使用经过验证的库,如
PyGazeAnalyser或直接使用眼动仪厂商提供的SDK进行注视点检测。这里的简化方法主要用于教学和快速原型验证。
4. 注视点轨迹图绘制:还原视觉扫描路径
有了清洗后的注视点数据,我们就可以开始绘制轨迹图了。轨迹图的目标是清晰展示注视点的空间位置、时间顺序以及相对持续时间。
4.1 基础轨迹绘制
我们将使用Matplotlib的scatter和plot函数。scatter用于绘制点(代表注视点),点的大小映射注视时长;plot用于绘制连接线,表示视线移动路径。
import matplotlib.pyplot as plt # 设置画布,背景可以加载实验所用的刺激图片 fig, ax = plt.subplots(figsize=(12, 8)) # 假设我们有一张背景图 'stimulus.jpg' # img = plt.imread('stimulus.jpg') # ax.imshow(img, extent=[0, screen_width, screen_height, 0]) # 注意y轴方向 # 绘制注视点,点的大小与注视时长成正比 # 这里对时长进行缩放,使得点的大小在视觉上区分明显 scatter_size = fixation_df['duration'] / fixation_df['duration'].max() * 500 # 缩放因子可调 scatter = ax.scatter(fixation_df['x'], fixation_df['y'], s=scatter_size, alpha=0.6, c='red', edgecolors='black', linewidth=0.5) # 绘制注视点之间的连线(按时间顺序) ax.plot(fixation_df['x'], fixation_df['y'], 'b-', alpha=0.4, linewidth=1) # 添加注视点编号(时间顺序) for i, row in fixation_df.iterrows(): ax.annotate(str(i+1), (row['x'], row['y']), fontsize=8, ha='center', va='center', color='white') # 设置坐标轴和标题 ax.set_xlim(0, screen_width) ax.set_ylim(screen_height, 0) # 注意:图像坐标系y轴向下,我们需要反转以匹配屏幕坐标 ax.set_xlabel('X Coordinate (pixels)') ax.set_ylabel('Y Coordinate (pixels)') ax.set_title('Fixation Sequence and Duration') ax.grid(True, alpha=0.3) plt.tight_layout() plt.show()4.2 高级增强:用颜色编码时间或兴趣区
为了使轨迹图信息量更大,我们可以用颜色来编码额外的维度。例如,用渐变色表示时间先后(从冷色到暖色),或者根据注视点所在的兴趣区(AOI)来着色。
# 颜色编码时间(归一化时间戳) fixation_df['time_norm'] = (fixation_df.index - fixation_df.index.min()) / (fixation_df.index.max() - fixation_df.index.min()) # 使用viridis色彩映射 colors = plt.cm.viridis(fixation_df['time_norm']) fig, ax = plt.subplots(figsize=(12, 8)) # 绘制带颜色编码的散点 scatter = ax.scatter(fixation_df['x'], fixation_df['y'], s=scatter_size, alpha=0.7, c=colors, edgecolors='k', linewidth=0.5) # 绘制连线,连线颜色也可以渐变 for i in range(len(fixation_df)-1): ax.plot([fixation_df.iloc[i]['x'], fixation_df.iloc[i+1]['x']], [fixation_df.iloc[i]['y'], fixation_df.iloc[i+1]['y']], color=colors[i], alpha=0.5, linewidth=1) # 添加颜色条表示时间 sm = plt.cm.ScalarMappable(cmap='viridis', norm=plt.Normalize(vmin=0, vmax=1)) sm.set_array([]) cbar = plt.colorbar(sm, ax=ax) cbar.set_label('Normalized Time Sequence') ax.set_xlim(0, screen_width) ax.set_ylim(screen_height, 0) ax.set_title('Fixation Sequence Colored by Time') plt.show()注意事项:轨迹图在注视点过多时会变得非常杂乱。一种常见的做法是只呈现前N个注视点,或者按时间分段呈现。另外,连线的交叉可能会造成视觉混淆,可以考虑使用曲线(如
ax.plot(..., '-', alpha=0.3))或完全省略连线,仅用编号表示顺序。
5. 注意力热图生成:量化视觉注意的密度分布
如果说轨迹图讲述的是“视觉旅程”的故事,那么热图呈现的就是“视觉停留”的总结。热图通过颜色深浅直观地告诉我们,屏幕上的哪些区域吸引了最多的注视总时长。
5.1 热图生成原理:从点到面
热图的本质是一个二维直方图或核密度估计(KDE)图。我们需要将离散的注视点(每个点带有“权重”,即注视时长)转换为一个覆盖整个屏幕的、连续的密度分布表面。
方法一:二维直方图(加权)这是最直观、计算最快的方法。将屏幕划分为网格(如50x50),统计每个网格内所有注视点的持续时间之和,这个和值就是该网格的“热度”值。
def generate_heatmap_histogram(fixation_df, screen_width, screen_height, grid_size=50): """ 通过加权二维直方图生成热图数据。 fixation_df: 包含 'x', 'y', 'duration' 的DataFrame grid_size: 网格数量(将生成 grid_size x grid_size 的矩阵) """ # 创建网格边界 x_edges = np.linspace(0, screen_width, grid_size + 1) y_edges = np.linspace(0, screen_height, grid_size + 1) # 计算二维直方图,权重为注视时长 heatmap, x_edges, y_edges = np.histogram2d( fixation_df['x'], fixation_df['y'], bins=[x_edges, y_edges], weights=fixation_df['duration'], # 关键:使用时长作为权重 density=False # 不标准化为概率密度 ) # 转置以使热图方向正确 (histogram2d 返回的矩阵需要转置才能与imshow匹配) heatmap = heatmap.T return heatmap, x_edges, y_edges heatmap_data, x_edges, y_edges = generate_heatmap_histogram(fixation_df, screen_width, screen_height) # 使用Matplotlib的imshow绘制 fig, ax = plt.subplots(figsize=(10, 6)) # extent参数定义了图像在数据坐标中的范围 im = ax.imshow(heatmap_data, extent=[0, screen_width, screen_height, 0], origin='upper', cmap='hot', interpolation='gaussian', aspect='auto') ax.set_xlabel('X Coordinate') ax.set_ylabel('Y Coordinate') ax.set_title('Attention Heatmap (Weighted by Fixation Duration)') plt.colorbar(im, ax=ax, label='Total Fixation Duration (ms)') plt.show()方法二:核密度估计(KDE)KDE能生成更平滑、更“连续”的热图。它假设每个注视点都是一个“核函数”(如高斯函数),最终的密度是所有这些核函数的叠加。SciPy提供了gaussian_kde函数,但它默认处理一维数据。对于二维加权KDE,我们需要一些技巧或使用scipy.stats。
from scipy import stats def generate_heatmap_kde(fixation_df, screen_width, screen_height, grid_size=200): """ 使用高斯核密度估计生成热图。 注意:对于大量数据点,计算可能较慢。 """ # 准备数据 x = fixation_df['x'].values y = fixation_df['y'].values # 如果考虑权重(时长),可以重复数据点来模拟权重(近似方法) # 更精确的方法需要使用能够接受权重的KDE函数,这里使用近似 weights = fixation_df['duration'].values weights_normalized = weights / weights.mean() # 归一化权重 # 创建一个放大的样本数组来近似权重(计算量大,慎用) # 这里为了演示,我们使用未加权的KDE kde = stats.gaussian_kde([x, y]) # 创建网格 xi = np.linspace(0, screen_width, grid_size) yi = np.linspace(0, screen_height, grid_size) Xi, Yi = np.meshgrid(xi, yi) # 评估KDE在网格点上的密度 Zi = kde(np.vstack([Xi.flatten(), Yi.flatten()])) Zi = Zi.reshape(Xi.shape) return Xi, Yi, Zi Xi, Yi, Zi = generate_heatmap_kde(fixation_df, screen_width, screen_height, grid_size=100) fig, ax = plt.subplots(figsize=(10, 6)) # 使用pcolormesh绘制平滑的密度图 cp = ax.pcolormesh(Xi, Yi, Zi, cmap='hot', shading='auto') ax.set_xlabel('X Coordinate') ax.set_ylabel('Y Coordinate') ax.set_title('Attention Heatmap (Gaussian KDE)') plt.colorbar(cp, ax=ax, label='Density Estimate') plt.show()5.2 热图绘制的关键参数与技巧
- 色彩映射(cmap):
'hot','plasma','viridis'是常用的热图配色。'hot'(白-黄-红-黑)对比强烈,最符合“热力”的直觉。'viridis'(绿-黄-蓝)是色盲友好且感知均匀的配色。 - 插值(interpolation):在
imshow中,interpolation='gaussian'或'bicubic'可以使热图更平滑,避免出现明显的马赛克块。'nearest'则保留清晰的网格边界。 - 平滑与阈值:原始热图可能噪声较多。可以对
heatmap_data应用高斯滤波(scipy.ndimage.gaussian_filter)进行平滑。也可以设置一个阈值,将低于该值的区域设为透明或背景色,以突出高关注区域。from scipy.ndimage import gaussian_filter smoothed_heatmap = gaussian_filter(heatmap_data, sigma=1.0) # sigma控制平滑程度 - 叠加背景图:将热图以半透明形式叠加在实验刺激图片上,是分析的标准做法,能提供最直观的上下文。
fig, ax = plt.subplots(figsize=(12, 8)) img = plt.imread('stimulus.jpg') ax.imshow(img, extent=[0, screen_width, screen_height, 0]) # 叠加半透明热图 im = ax.imshow(smoothed_heatmap, extent=[0, screen_width, screen_height, 0], origin='upper', cmap='hot', alpha=0.6, interpolation='gaussian') ax.set_title('Heatmap Overlay on Stimulus') plt.colorbar(im, ax=ax, label='Attention Density') plt.show()
实操心得:直方图法速度快,结果稳定,易于解释(每个格子的值就是总注视时长),是报告中的首选。KDE法更美观、平滑,更能反映注意力的“扩散”效应,但计算慢,且带宽参数的选择对结果影响很大,需要谨慎调整。在大多数实际项目中,使用加权的直方图法并辅以高斯平滑,就能得到既美观又可靠的结果。
6. 完整项目实战:构建可复用的分析流水线
现在,我们将以上所有步骤整合到一个模块化、可配置的Python脚本或Jupyter Notebook中,形成一个完整的分析流水线。这不仅能用于单个数据文件,更能通过循环处理批量数据,实现自动化报告生成。
6.1 项目结构设计
建议创建一个结构清晰的目录和模块:
eyetracking_visualization/ │ ├── config.py # 配置文件(屏幕分辨率、路径、参数等) ├── data_loader.py # 数据加载与清洗模块 ├── fixation_detection.py # 注视点检测模块(可使用简化或高级算法) ├── visualization.py # 轨迹图和热图绘制函数 ├── utils.py # 工具函数(如平滑、坐标转换) ├── main.py # 主程序,串联整个流程 └── outputs/ # 存放生成的图片和报告6.2 核心模块代码示例
config.py:
SCREEN_RESOLUTION = (1920, 1080) # (width, height) DATA_PATH = './data/raw/' OUTPUT_PATH = './outputs/' HEATMAP_GRID_SIZE = 80 FIXATION_TIME_WINDOW_MS = 100 FIXATION_SPATIAL_THRESHOLD_PX = 25visualization.py(核心绘图函数封装):
import matplotlib.pyplot as plt import numpy as np import os def plot_fixation_sequence(fixation_df, background_img_path=None, output_path=None, **kwargs): """绘制注视点序列轨迹图""" fig, ax = plt.subplots(figsize=kwargs.get('figsize', (12, 8))) screen_width = kwargs.get('screen_width', 1920) screen_height = kwargs.get('screen_height', 1080) # 加载背景图 if background_img_path and os.path.exists(background_img_path): img = plt.imread(background_img_path) ax.imshow(img, extent=[0, screen_width, screen_height, 0]) # 计算点大小 max_duration = fixation_df['duration'].max() min_point_size = kwargs.get('min_point_size', 50) max_point_size = kwargs.get('max_point_size', 800) if max_duration > 0: point_sizes = min_point_size + (fixation_df['duration'] / max_duration) * (max_point_size - min_point_size) else: point_sizes = np.full(len(fixation_df), min_point_size) # 绘制散点和连线 scatter = ax.scatter(fixation_df['x'], fixation_df['y'], s=point_sizes, alpha=0.7, c='red', edgecolors='black', linewidth=1.5, zorder=5) ax.plot(fixation_df['x'], fixation_df['y'], 'gray', alpha=0.4, linewidth=1, zorder=4) # 添加编号 for i, row in fixation_df.iterrows(): ax.annotate(str(i+1), (row['x'], row['y']), fontsize=9, ha='center', va='center', color='white', weight='bold', zorder=6) ax.set_xlim(0, screen_width) ax.set_ylim(screen_height, 0) ax.set_xlabel('X Coordinate (pixels)') ax.set_ylabel('Y Coordinate (pixels)') ax.set_title(kwargs.get('title', 'Fixation Sequence Plot')) ax.grid(False) # 通常叠加在图片上时关闭网格 if output_path: plt.savefig(output_path, dpi=300, bbox_inches='tight') print(f"轨迹图已保存至: {output_path}") else: plt.show() plt.close(fig) def plot_attention_heatmap(fixation_df, background_img_path=None, output_path=None, method='histogram', **kwargs): """绘制注意力热图""" screen_width = kwargs.get('screen_width', 1920) screen_height = kwargs.get('screen_height', 1080) grid_size = kwargs.get('grid_size', 80) # 生成热图数据 if method == 'histogram': heatmap_data, x_edges, y_edges = generate_heatmap_histogram(fixation_df, screen_width, screen_height, grid_size) # 可选平滑 if kwargs.get('smooth', True): from scipy.ndimage import gaussian_filter heatmap_data = gaussian_filter(heatmap_data, sigma=kwargs.get('sigma', 1.0)) extent = [0, screen_width, screen_height, 0] elif method == 'kde': Xi, Yi, Zi = generate_heatmap_kde(fixation_df, screen_width, screen_height, grid_size=grid_size) heatmap_data = Zi extent = [Xi.min(), Xi.max(), Yi.max(), Yi.min()] # 注意y轴方向 else: raise ValueError("method must be 'histogram' or 'kde'") fig, ax = plt.subplots(figsize=kwargs.get('figsize', (12, 8))) # 加载背景图 if background_img_path and os.path.exists(background_img_path): img = plt.imread(background_img_path) ax.imshow(img, extent=[0, screen_width, screen_height, 0]) # 叠加热图 im = ax.imshow(heatmap_data, extent=extent, origin='upper', cmap=kwargs.get('cmap', 'hot'), alpha=kwargs.get('alpha', 0.6), interpolation=kwargs.get('interpolation', 'gaussian')) ax.set_xlabel('X Coordinate') ax.set_ylabel('Y Coordinate') ax.set_title(kwargs.get('title', 'Attention Heatmap')) plt.colorbar(im, ax=ax, label='Attention Intensity') if output_path: plt.savefig(output_path, dpi=300, bbox_inches='tight') print(f"热图已保存至: {output_path}") else: plt.show() plt.close(fig)6.3 主程序示例
main.py:
import pandas as pd from config import * from data_loader import load_and_clean_data from fixation_detection import identify_fixations from visualization import plot_fixation_sequence, plot_attention_heatmap import os def main(data_file, stimulus_image): """处理单个数据文件""" print(f"正在处理文件: {data_file}") # 1. 加载与清洗 df_raw = pd.read_csv(os.path.join(DATA_PATH, data_file)) df_clean = load_and_clean_data(df_raw, SCREEN_RESOLUTION) # 2. 注视点检测 fixation_df = identify_fixations(df_clean, time_window=FIXATION_TIME_WINDOW_MS, spatial_threshold=FIXATION_SPATIAL_THRESHOLD_PX) print(f" 识别出 {len(fixation_df)} 个注视点") # 3. 可视化 output_prefix = os.path.splitext(data_file)[0] # 轨迹图 traj_output = os.path.join(OUTPUT_PATH, f"{output_prefix}_trajectory.png") plot_fixation_sequence(fixation_df, background_img_path=stimulus_image, output_path=traj_output, screen_width=SCREEN_RESOLUTION[0], screen_height=SCREEN_RESOLUTION[1], title=f'Fixation Trajectory - {output_prefix}') # 热图 heatmap_output = os.path.join(OUTPUT_PATH, f"{output_prefix}_heatmap.png") plot_attention_heatmap(fixation_df, background_img_path=stimulus_image, output_path=heatmap_output, method='histogram', screen_width=SCREEN_RESOLUTION[0], screen_height=SCREEN_RESOLUTION[1], grid_size=HEATMAP_GRID_SIZE, title=f'Attention Heatmap - {output_prefix}') print(f" 可视化结果已保存至 {OUTPUT_PATH}") if __name__ == '__main__': # 示例:处理一个文件 main('participant_01_trial_1.csv', 'stimulus_webpage.jpg') # 批量处理 # for file in os.listdir(DATA_PATH): # if file.endswith('.csv'): # main(file, 'common_stimulus.jpg')通过这样的模块化设计,你可以轻松地调整参数、更换算法、处理新数据,并将整个流程集成到更大型的数据分析平台中。
7. 常见问题、排查技巧与性能优化
在实际操作中,你一定会遇到各种预料之外的情况。下面是我总结的一些典型问题及其解决方案。
7.1 数据与可视化问题排查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 热图一片空白或只有几个点 | 1. 坐标超出画布范围。 2. 数据清洗过于严格,过滤掉了大部分点。 3. 热图色彩映射范围不合适。 | 1. 打印fixation_df的x,y的min()和max(),确认其在屏幕分辨率内。2. 检查清洗步骤的阈值是否设得太小(如空间阈值)。临时放宽条件测试。 3. 检查 heatmap_data矩阵的值,可能都非常小。尝试对数据取对数或调整imshow的vmin,vmax参数。 |
| 轨迹图连线交叉混乱 | 注视点过多,或扫描路径本身复杂。 | 1. 考虑不绘制连线,仅用编号表示顺序。 2. 使用颜色渐变表示时间顺序,替代连线。 3. 只绘制前20-30个注视点(对于长时间任务)。 |
| 热图有奇怪的条纹或块状伪影 | 1. 网格划分太粗糙(grid_size太小)。2. 使用了 interpolation='nearest'。3. 原始数据存在系统误差(如眼动仪校准问题)。 | 1. 增加grid_size(如从30提高到80或120)。2. 使用 interpolation='gaussian'或'bicubic'。3. 对原始数据应用轻微的高斯平滑,或检查数据采集日志。 |
| 图形保存后分辨率太低 | plt.savefig的dpi参数设置过低。 | 在savefig中明确设置dpi=300或更高。同时确保画布尺寸(figsize)足够大。 |
| 注视点识别数量异常少 | 注视点检测算法的参数(时间窗口、空间阈值)不适合当前数据。 | 1. 可视化原始数据点,观察其空间分布和采样间隔。 2. 尝试不同的参数组合。一个经验法则是:空间阈值约为屏幕宽度的1-2%,时间窗口为100-200ms。使用参数网格搜索,并与人工标注的少量数据进行对比验证。 |
| 叠加背景图后坐标错位 | 眼动坐标坐标系与图像坐标系不匹配。 | 1. 确认眼动坐标原点(通常是屏幕左上角)和图像显示区域是否完全对齐。 2. 检查 ax.imshow的extent参数是否正确设置为[0, width, height, 0](注意y轴顺序)。3. 如果刺激图片在屏幕上不是全屏显示,需要进行坐标变换。 |
7.2 性能优化技巧
大数据处理:当处理数十万甚至上百万的采样点时,Pandas操作和KDE计算会非常慢。
- 向量化操作:尽量使用NumPy/Pandas的向量化函数,避免Python层面的
for循环。 - 分块处理:对于超大数据,可以考虑按时间或按试次分块处理,再合并结果。
- 近似算法:对于热图,直方图法远快于KDE。如果必须用KDE,可以尝试
statsmodels库的KDEMultivariate或使用更快的近似算法,或对数据进行下采样后再计算。 - 使用更高效的数据结构:在注视点检测等计算密集型步骤,将DataFrame列转换为NumPy数组进行操作,速度会快很多。
x_values = df['gaze_point_x'].to_numpy() y_values = df['gaze_point_y'].to_numpy() # 在数组上进行计算...
- 向量化操作:尽量使用NumPy/Pandas的向量化函数,避免Python层面的
自动化与批处理:利用Python的
os和glob模块遍历文件夹内所有数据文件,结合multiprocessing库进行多进程并行处理,可以极大提升批量生成报告的效率。
7.3 扩展方向与高级应用
掌握了基础的可视化后,你可以向更深入的分析迈进:
- 兴趣区(AOI)分析:定义屏幕上特定的矩形或圆形区域(如网页的Logo、导航栏、购买按钮),统计落在每个AOI内的注视点次数、总时长、首次进入时间等指标。这需要额外的AOI坐标定义和空间判断逻辑。
- 扫描路径比较:计算不同被试或不同条件之间扫描路径的相似性度量,如字符串编辑距离、向量相似度等。
- 动态热图/轨迹:使用
matplotlib.animation或imageio库生成动态GIF或视频,展示注视点随时间推移的累积过程,这对于演示和汇报极具冲击力。 - 与其它数据源整合:将眼动数据与鼠标点击、键盘事件、生理信号(如EEG、GSR)时间同步,进行多模态行为分析。
- 交互式可视化:使用
Plotly或Bokeh库创建交互式网页图表,允许查看者悬停查看注视点详情、切换不同被试的数据等。
眼动数据的价值远不止于两张图。通过Python构建起这套灵活的分析管道,你便拥有了将海量注视数据转化为深刻行为洞察的钥匙。从清晰的轨迹到灼热的热图,每一次可视化都在讲述一个关于“如何看”的故事。我个人的体会是,最大的挑战往往不在编码,而在于理解数据背后的实验逻辑和人类视觉认知原理,从而做出合理的参数选择和结果解读。希望这份详尽的指南能帮你避开我当年踩过的坑,更快地踏上眼动数据分析的实战之路。如果在复现过程中遇到任何问题,不妨回头检查一下数据清洗的步骤,那通常是所有问题的源头。
本文还有配套的精品资源,点击获取