-
Notifications
You must be signed in to change notification settings - Fork 45
/
gazeheatplot.py
229 lines (189 loc) · 8 KB
/
gazeheatplot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import os
import argparse
import csv
import numpy
import matplotlib
from matplotlib import pyplot, image
def draw_display(dispsize, imagefile=None):
"""Returns a matplotlib.pyplot Figure and its axes, with a size of
dispsize, a black background colour, and optionally with an image drawn
onto it
arguments
dispsize - tuple or list indicating the size of the display,
e.g. (1024,768)
keyword arguments
imagefile - full path to an image file over which the heatmap
is to be laid, or None for no image; NOTE: the image
may be smaller than the display size, the function
assumes that the image was presented at the centre of
the display (default = None)
returns
fig, ax - matplotlib.pyplot Figure and its axes: field of zeros
with a size of dispsize, and an image drawn onto it
if an imagefile was passed
"""
# construct screen (black background)
screen = numpy.zeros((dispsize[1], dispsize[0], 3), dtype='float32')
# if an image location has been passed, draw the image
if imagefile != None:
# check if the path to the image exists
if not os.path.isfile(imagefile):
raise Exception("ERROR in draw_display: imagefile not found at '%s'" % imagefile)
# load image
img = image.imread(imagefile)
# width and height of the image
w, h = len(img[0]), len(img)
# x and y position of the image on the display
x = dispsize[0] / 2 - w / 2
y = dispsize[1] / 2 - h / 2
# draw the image on the screen
screen[y:y + h, x:x + w, :] += img
# dots per inch
dpi = 100.0
# determine the figure size in inches
figsize = (dispsize[0] / dpi, dispsize[1] / dpi)
# create a figure
fig = pyplot.figure(figsize=figsize, dpi=dpi, frameon=False)
ax = pyplot.Axes(fig, [0, 0, 1, 1])
ax.set_axis_off()
fig.add_axes(ax)
# plot display
ax.axis([0, dispsize[0], 0, dispsize[1]])
ax.imshow(screen) # , origin='upper')
return fig, ax
def gaussian(x, sx, y=None, sy=None):
"""Returns an array of numpy arrays (a matrix) containing values between
1 and 0 in a 2D Gaussian distribution
arguments
x -- width in pixels
sx -- width standard deviation
keyword argments
y -- height in pixels (default = x)
sy -- height standard deviation (default = sx)
"""
# square Gaussian if only x values are passed
if y == None:
y = x
if sy == None:
sy = sx
# centers
xo = x / 2
yo = y / 2
# matrix of zeros
M = numpy.zeros([y, x], dtype=float)
# gaussian matrix
for i in range(x):
for j in range(y):
M[j, i] = numpy.exp(
-1.0 * (((float(i) - xo) ** 2 / (2 * sx * sx)) + ((float(j) - yo) ** 2 / (2 * sy * sy))))
return M
def draw_heatmap(gazepoints, dispsize, imagefile=None, alpha=0.5, savefilename=None, gaussianwh=200, gaussiansd=None):
"""Draws a heatmap of the provided fixations, optionally drawn over an
image, and optionally allocating more weight to fixations with a higher
duration.
arguments
gazepoints - a list of gazepoint tuples (x, y)
dispsize - tuple or list indicating the size of the display,
e.g. (1024,768)
keyword arguments
imagefile - full path to an image file over which the heatmap
is to be laid, or None for no image; NOTE: the image
may be smaller than the display size, the function
assumes that the image was presented at the centre of
the display (default = None)
alpha - float between 0 and 1, indicating the transparancy of
the heatmap, where 0 is completely transparant and 1
is completely untransparant (default = 0.5)
savefilename - full path to the file in which the heatmap should be
saved, or None to not save the file (default = None)
returns
fig - a matplotlib.pyplot Figure instance, containing the
heatmap
"""
# IMAGE
fig, ax = draw_display(dispsize, imagefile=imagefile)
# HEATMAP
# Gaussian
gwh = gaussianwh
gsdwh = gwh / 6 if (gaussiansd is None) else gaussiansd
gaus = gaussian(gwh, gsdwh)
# matrix of zeroes
strt = gwh / 2
heatmapsize = dispsize[1] + 2 * strt, dispsize[0] + 2 * strt
heatmap = numpy.zeros(heatmapsize, dtype=float)
# create heatmap
for i in range(0, len(gazepoints)):
# get x and y coordinates
x = strt + gazepoints[i][0] - int(gwh / 2)
y = strt + gazepoints[i][1] - int(gwh / 2)
# correct Gaussian size if either coordinate falls outside of
# display boundaries
if (not 0 < x < dispsize[0]) or (not 0 < y < dispsize[1]):
hadj = [0, gwh];
vadj = [0, gwh]
if 0 > x:
hadj[0] = abs(x)
x = 0
elif dispsize[0] < x:
hadj[1] = gwh - int(x - dispsize[0])
if 0 > y:
vadj[0] = abs(y)
y = 0
elif dispsize[1] < y:
vadj[1] = gwh - int(y - dispsize[1])
# add adjusted Gaussian to the current heatmap
try:
heatmap[y:y + vadj[1], x:x + hadj[1]] += gaus[vadj[0]:vadj[1], hadj[0]:hadj[1]] * gazepoints[i][2]
except:
# fixation was probably outside of display
pass
else:
# add Gaussian to the current heatmap
heatmap[y:y + gwh, x:x + gwh] += gaus * gazepoints[i][2]
# resize heatmap
heatmap = heatmap[strt:dispsize[1] + strt, strt:dispsize[0] + strt]
# remove zeros
lowbound = numpy.mean(heatmap[heatmap > 0])
heatmap[heatmap < lowbound] = numpy.NaN
# draw heatmap on top of image
ax.imshow(heatmap, cmap='jet', alpha=alpha)
# FINISH PLOT
# invert the y axis, as (0,0) is top left on a display
ax.invert_yaxis()
# save the figure if a file name was provided
if savefilename != None:
fig.savefig(savefilename)
return fig
##################
# Parsing #
##################
parser = argparse.ArgumentParser(description='Parameters required for processing.')
#required args
parser.add_argument('input-path', type=str, help='path to the csv input')
parser.add_argument('display-width', type=int, help='an integer representing the display width')
parser.add_argument('display-height', type=int, help='an integer representing the display height')
#optional args
parser.add_argument('-a', '--alpha', type=float, default='0.5', required=False, help='alpha for the gaze overlay')
parser.add_argument('-o', '--output-name', type=str, required=False, help='name for the output file')
parser.add_argument('-b', '--background-image', type=str, default=None, required=False, help='path to the background image')
#advanced optional args
parser.add_argument('-n', '--n-gaussian-matrix', type=int, default='200', required=False, help='width and height of gaussian matrix')
parser.add_argument('-sd', '--standard-deviation', type=float, default=None ,required=False, help='standard deviation of gaussian distribution')
args = vars(parser.parse_args())
input_path = args['input-path']
display_width = args['display-width']
display_height = args['display-height']
alpha = args['alpha']
output_name = args['output_name'] if args['output_name'] is not None else 'output'
background_image = args['background_image']
ngaussian = args['n_gaussian_matrix']
sd = args['standard_deviation']
with open(input_path) as f:
reader = csv.reader(f)
raw = list(reader)
gaza_data = []
if len(raw[0]) is 2:
gaze_data = list(map(lambda q: (int(q[0]), int(q[1]), 1), raw))
else:
gaze_data = list(map(lambda q: (int(q[0]), int(q[1]), int(q[2])), raw))
draw_heatmap(gaze_data, (display_width, display_height), alpha=alpha, savefilename=output_name, imagefile=background_image, gaussianwh=ngaussian, gaussiansd=sd)