概要
ユーザーが広告のどの部分をどのくらい注目したのかを、視線データを使って可視化する方法を紹介します。
調査内容(仮)
Eye Tracking(視線追跡)デバイスを使い、動画の途中で広告を表示しました。
興味のある場所はじっと見てもらい、なければ視線を外してもよいと伝えています。
📺広告画像
📺視線の時間と順番をマーカーで可視化した完成図
ユーザーが見た場所に「見た時間」のマーカーサイズと「見た順番」の番号を表示します。
視線データ(サンプル用に作成)
ファイル名:サンプル.csv
カラム名 | 説明 |
---|---|
id | ユニークID |
time | 時間(サンプルでは連番) |
position_x | 視線のx座標 |
position_y | 視線のy座標 |
画像ファイル
fish.png
サイズ:1360×840
環境
python3.x
jupyter lab
コード解説
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.image as mpimg
import japanize_matplotlib
📌import japanize_matplotlib
matplotlibで日本語を正しく表示するためのライブラリです。
グラフのタイトルや軸ラベルの文字化けを防ぎます。
#=============================================
# Inputデータ読み込む
#=============================================
df = pd.read_csv("サンプル.csv",encoding='UTF-8')
#=============================================
# 近い点をまとめる関数(前の点と距離20以内なら同じ座標に)
#=============================================
def merge_close_points(x, y, threshold=20):
# 近い点をまとめる
merged_x = []
merged_y = []
for i in range(len(x)):
if i == 0:
merged_x.append(x[i])
merged_y.append(y[i])
else:
dx = x[i] - merged_x[-1]
dy = y[i] - merged_y[-1]
dist = np.sqrt(dx*dx + dy*dy)
if dist <= threshold:
merged_x.append(merged_x[-1])
merged_y.append(merged_y[-1])
else:
merged_x.append(x[i])
merged_y.append(y[i])
# 連続グループ番号付け
group_numbers = []
group_id = 0
for i in range(len(merged_x)):
if i == 0:
group_numbers.append(group_id)
else:
if merged_x[i] != merged_x[i-1] or merged_y[i] != merged_y[i-1]:
group_id += 1
group_numbers.append(group_id)
return merged_x, merged_y, group_numbers
merged_x, merged_y, group_numbers = merge_close_points(df['position_x'], df['position_y'])
📒 近い点をまとめる
📌threshold=20
座標を丸める(まとめる)距離の閾値。
この値以下の距離の点は同じ位置として扱う。
📌merged_x = [] / merged_y = []
まとめた後のx座標、y座標を格納する空リストを用意。
📌 for i in range(len(x)):
x座標リストの全要素を順番に繰り返し処理。
📌 if i == 0
merged_x.append(x[i]) / merged_y.append(y[i])
最初の点の場合はそのままmerged_xとmerged_yに追加。
📌 else:
dx = x[i] - merged_x[-1] / dy = y[i] - merged_y[-1]
現在の点と直前にまとめた点のx,y座標の差分を計算。
この差分を使って2点間の距離を算出し、まとめるかどうか判定。
📌 dist = np.sqrt(dxdx + dydy)
差分からユークリッド距離(点間距離)を計算。
📌📌 if dist <= threshold
merged_x.append(merged_x[-1]) / merged_y.append(merged_y[-1])
距離が閾値以下なら、前の点と同じ座標としてまとめる。
📌📌 else
merged_x.append(x[i]) / merged_y.append(y[i])
距離が閾値より大きければ(丸めずに)新たな座標として追加。
📒 連続グループ番号付け
📌 group_numbers = [] / group_id = 0
まとめた座標にグループ番号を割り振る準備。
→ グループ番号:同じ位置にまとまった点の連続番号。
📌 for i in range(len(merged_x)):
まとめた座標リストの全要素を順に処理。
📌 if i == 0:
group_numbers.append(group_id)
座標のスタート地点は グループ番号=0
にする。
📌 else:
if merged_x[i] != merged_x[i-1] or merged_y[i] != merged_y[i-1]:
前の点と座標が違えばグループ番号を1増やす。
📌 group_numbers.append(group_id)
現在のグループ番号(連続する同じ座標の塊に付けた連番)をリストに追加。
📌merged_x, merged_y, group_numbers = merge_close_points(df['position_x'], df['position_y'])
関数を呼び出して、処理結果をまとめて変数に代入。
❓なぜ座標を丸めるの❓
視線データは微妙にブレることがあります。
そのため、距離が近い点を同じ箇所としてまとめることで、同じ場所を見ていることを意味付けています。
これが閾値を設定する理由です。
#=============================================
# 画像読み込み
#=============================================
img = mpimg.imread("fish.png")
plt.figure(figsize=(13.8, 8.6))
plt.imshow(img)
# 軌跡の線(黒)
plt.plot(merged_x, merged_y, color='black', linewidth=1)
# 点の大きさと色
sizes = df['time'] * 10
colors = df['time']
scatter = plt.scatter(merged_x, merged_y, s=sizes, c=colors, cmap='viridis', alpha=0.8)
plt.colorbar(scatter, label='time')
# 画像の座標系に合わせて軸を設定(左上0,0)
plt.xlim(0, img.shape[1])
plt.ylim(img.shape[0], 0) # Y軸反転
plt.xlabel("position_x")
plt.ylabel("position_y")
plt.title("視線軌跡 with Gradient Color and Labels")
# 点の中心に time の数字を入れる
for x, y, g, s in zip(merged_x, merged_y, group_numbers, sizes):
fontsize = np.sqrt(s) # 面積の平方根を文字サイズに
plt.text(x, y, str(g), color='red', fontsize=fontsize, ha='center', va='center')
plt.show()
📌 img = mpimg.imread("fish_salmon.png")
画像ファイルを読み込み、NumPy配列として取得。
📌 plt.figure(figsize=(13.8, 8.6))
描画する図のサイズ(幅13.8インチ、高さ8.6インチ)を設定。
📌 plt.plot(merged_x, merged_y, color='black', linewidth=1)
視線軌跡を黒い線で描画。
📌 sizes = df['time'] * 10
点の大きさをtime列の値
を10倍にする。
→ 必要に応じてサイズを調整できます。
📌 colors = df['time']
点の色をtimeの値でグラデーション指定。
📌 scatter = plt.scatter(...)
視線ポイントを大きさsizes
、色colors
、透明度0.8
で散布図として表示。
📌 plt.colorbar(scatter, label='time')
色の凡例(カラーバー)を追加し、ラベルは'time'。
📌 plt.xlim(0, img.shape[1])
X軸の表示範囲を画像の横幅(ピクセル数)に合わせる。
→ 背景画像とプロット(視線ポイントや軌跡)がぴったり重なります。
📌 plt.ylim(img.shape[0], 0)
Y軸の範囲を画像の高さに合わせ上下を反転。
→ 画像座標系(左上原点)にプロット座標を合わせています。
📌 for x, y, g, s in zip(merged_x, merged_y, group_numbers, sizes):
4つのリストの要素をセットにして順番に取り出しながらループ処理。
-
x
: まとめたx座標 -
y
: まとめたy座標 -
g
: その点のグループ番号(連続した同じ位置の塊のID) -
s
: 点の大きさ(視線の注視時間に基づく)
ループ内でそれぞれの視線ポイントの情報を使って
位置(x, y)にグループ番号(g)をサイズ(s)に応じた文字サイズで描画します。
📌 fontsize = np.sqrt(s)
点の大きさに合わせて文字サイズを調整。
📌 plt.text(x, y, str(g), color='red', fontsize=fontsize, ha='center', va='center')
点の中心にグループ番号g
を赤文字で表示。
→ 文字の中心が座標と一致するよう調整。
❓なぜ背景の画像と、プロットのサイズが一致してるの?❓
- plt.xlim(0, img.shape[1]) でX軸の範囲を画像の横幅に合わせているため、視線データのプロットが画像の幅と一致します。
- plt.ylim(img.shape[0], 0) でY軸の範囲を画像の高さに合わせつつ上下反転し、画像座標系(左上原点)と一致させています。
- plt.figure(figsize=(13.8, 8.6)) で描画サイズを設定し、見た目のバランスや表示・保存時の品質を調整しています。
これらを揃えることで、背景画像に対して視線の軌跡やポイントが正確に重なるように表示できます
#=============================================
# 画像保存
#=============================================
plt.savefig("output_trajectory.png", dpi=300, bbox_inches='tight')
plt.show()
📌 dpi=300, bbox_inches='tight'
dpi=300 は画像の解像度(dots per inch)。
bbox_inches='tight' は図の余白を詰めて保存するための設定。
まとめ
今回は1人分のサンプルを使った可視化でしたが、多くのサンプルが集まれば
・どの広告部分に多くの人が注目しているか
・逆にあまり見られていない部分はどこか
が統計的に見えてきます。
広告だけでなく「人気の商品」や「注目されるデザイン」も一目でわかります。
「みんなこのあたりに注目しているな」とか「ここはあまり見られていないな」という傾向がわかるので、
商品の魅力アップやデザインの改善にも活かせます。
もちろん視線だけで全部判断するのは危険なので、
売上やクリック率、アンケート結果なんかと組み合わせるのが理想。
でも、視線の可視化は「人の興味がどこにあるか」をパッと掴むのにすごく便利な方法だと思います。