· 

注視点のヒートマップを作成する(Python)

(著)山たー

注視点のヒートマップを作成する。

追記:自分で実装しました

https://github.com/takyamamoto/Fixation-Densitymap

 

Matlabでの実装では

https://github.com/cvzoya/fixation-visualization

があるが、今回はPyGazeの実装を改変した

https://github.com/r0ehre/GazePointHeatMap

を使ってみた。

 

コード

gazeheatplot.py

import os
import argparse
import csv
import numpy as np
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 = np.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 = np.zeros([y, x], dtype=float)
    # gaussian matrix
    for i in range(x):
        for j in range(y):
            M[j, i] = np.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 = np.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 = np.mean(heatmap[heatmap > 0])
    heatmap[heatmap < lowbound] = np.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)

 

オプションは実行時に記述する。

python gazeheatplot.py gaze-data.csv 1440 900 [-a 0.6] [-o output-name] [-b /Me/bg-image.png] [-n 200] [-sd 33]

 

注視点分布の可視化

注視点データを可視化してみる。MIT Saliency Benchmarkにあるデータセット"CAT2000"を使ってみる。

 

mat2csv.py

import scipy.io as sio
import numpy as np

#matdata全体の読み込み
matdata = sio.loadmat("./FIXATIONLOCS/Affective/001.mat")
fixLocs=matdata["fixLocs"]

#matファイルをメモリから消去する
matdata.clear()

#位置を取得
fixPos_tmp=np.where(fixLocs==1)
fixPos=np.zeros((369,2))

#x,yを入れ替え
fixPos[:,0]=fixPos_tmp[1]
fixPos[:,1]=fixPos_tmp[0]

fixPos=fixPos.astype(np.int64)

#csvとして出力
np.savetxt('Affective_001.csv',fixPos,fmt="%.0f",delimiter=',')

matファイルをcsvファイルに変換。

 

jpg2png.py

from PIL import Image
im = Image.open('./Stimuli/Affective/001.jpg')
im.save('./Stimuli/Affective/001.png', quality=95)

PILでjpgをpngにしておく。こうしないと色がおかしくなった。

 

結果

 activate py27_32
python gazeheatplot.py Affective_001.csv 1920 1080 -a 0.6 -o output.png -b ./Stimuli/Affective/001.png -n 200 -sd 33

 

コメントをお書きください

コメント: 1
  • #1

    和田哲也 (水曜日, 05 8月 2020 06:57)

    参考にさせていただいております。
    python3で動くように改変予定はないでしょうか?