VAS値・左右瞳孔径・瞬き検出による計測(ソースコードと実行結果)


Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine Codeium.Windsurf -e --silent
【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
必要なライブラリのインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する
前準備: pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install opencv-python scikit-learn pillow tqdm numpy
VAS値・左右瞳孔径・瞬き検出による計測プログラム
# VAS値・左右虹彩径・瞬き検出による心理生理状態計測システム
# 計測データ:
# - VAS(Visual Analog Scale)値:0-100の主観的評価
# - 左右虹彩径:個別に計測・正規化
# - 虹彩径左右差:パーセンテージと状態評価
# - 虹彩径変動係数:測定値の変動性指標
# - 瞬き検出:タイムスタンプ付き
# - 瞬き頻度:10秒ごとの回数と分あたり換算
# 使用技術: MediaPipe Face Mesh による虹彩輪郭計測
# 学習済みモデル: MediaPipe Face Mesh(内蔵モデル)
# 方式設計:
# - 入力: カメラ映像、VASスライダー入力(0-100)
# - 出力: リアルタイム表示、結果ファイル(result.txt)
# - 処理手順: 顔画像取得→ランドマーク検出→虹彩径計測→統計分析→表示
# - 前処理: 顔検出信頼度フィルタリング(0.5以上)
# - 後処理: 移動平均によるノイズ除去(10フレーム)
# 注意事項: 本システムは虹彩輪郭から径を計測しており、瞳孔径の直接測定ではありません
# 前準備: pip install opencv-python mediapipe numpy matplotlib japanize-matplotlib scipy pillow
import cv2
import mediapipe as mp
import numpy as np
import tkinter as tk
from collections import deque
import time
import matplotlib.pyplot as plt
import japanize_matplotlib
from scipy import stats
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime
# プログラム起動直後の解説表示
print('========== VAS値・左右虹彩径・瞬き検出による心理生理状態計測システム ==========')
print()
print('【システム概要】')
print('本システムは、Visual Analog Scale(VAS)による主観的評価と、')
print('虹彩径計測による客観的な指標を同時計測することで、')
print('意識的・無意識的な心理生理状態を包括的に評価します。')
print()
print('【Visual Analog Scale (VAS) について】')
print()
print('VASは医療・心理学分野で広く使用される主観的評価尺度です。')
print('連続的な線分上で対象者が自身の状態を示すことで、')
print('痛み、不快感、不安などの主観的体験を定量化します。')
print()
print('VASの特徴:')
print('- 0から100の連続尺度による評価')
print('- 言語化困難な感覚の数値化が可能')
print('- 時系列変化の追跡に適している')
print('- 文化や言語の影響を受けにくい')
print()
print('【VASと生理指標の関連分析】')
print()
print('本システムでは、VAS(主観的評価)と以下の客観的生理指標を同時計測し、')
print('相関関係を分析することで、以下の知見が得られます:')
print()
print('1. VAS値と虹彩径の相関')
print(' - 認知負荷や覚醒状態の変化に伴う虹彩径の変動分析')
print(' - 主観的評価と客観的指標の関係性評価')
print()
print('2. VAS値と虹彩径変動係数の関係')
print(' - 高VAS値での変動係数増大:測定環境や状態の不安定性')
print(' - 心理的動揺の計測値への影響評価')
print()
print('3. VAS値と瞬き頻度の関連')
print(' - 不快感増大時の瞬き頻度変化')
print(' - 注意資源の配分変化を反映')
print()
print('4. VAS値と左右虹彩径差の関係')
print(' - 測定条件の変化による非対称性の評価')
print(' - 計測精度の指標として活用')
print()
print('【臨床的意義】')
print()
print('- 主観的訴えの客観的指標との比較')
print('- 測定状態の安定性評価')
print('- 認知負荷の多面的評価')
print('- 測定品質の指標として活用可能')
print()
print('【計測原理と科学的根拠】')
print()
print('1. 虹彩径計測')
print(' 虹彩の水平直径は約11.7±0.5mmで個人間で比較的一定です。')
print(' - 虹彩径は光の変化では変動しません(瞳孔径とは異なる)')
print(' - 個人差を正規化し、変動係数により測定安定性を定量化します')
print(' - 顔の角度や距離による見かけ上の変化を分析対象とします')
print()
print('2. 虹彩径左右差分析')
print(' 健常者では左右の虹彩径はほぼ同等です(差は通常5%以内)。')
print(' 10%を超える左右差は、測定条件の変化や顔の角度変化を示唆します。')
print()
print('3. 瞬き頻度分析')
print(' 正常な瞬き頻度は15-20回/分です。')
print(' - 10回/分未満:過度の集中、視覚的注意の固定、疲労の蓄積')
print(' - 30回/分以上:眼精疲労、ストレス、不安状態')
print()
print('【測定値の解釈】')
print()
print('- VAS値:0(問題なし)から100(最大の不快感)の主観的評価')
print('- 虹彩径:基準値を100%とした相対値。顔の角度や距離により変動')
print('- 変動係数:虹彩径の変動性。測定環境の安定性を反映')
print('- 左右差:測定条件の対称性指標。5%以内が理想的')
print('- 瞬き頻度:覚醒度と疲労度の指標')
print()
print('【活用例】')
print('- 作業環境の客観的評価')
print('- 測定状態のモニタリング')
print('- 疲労の早期検出')
print('- 認知負荷の定量化')
print()
print('【測定上の注意事項】')
print('1. 一定の照明環境で測定してください(照明変化は計測精度に影響)')
print('2. 測定開始後10秒間は基準値設定のため安静にしてください')
print('3. コンタクトレンズや眼鏡は測定に影響しません')
print('4. 本システムは虹彩径を計測しており、瞳孔径とは異なります')
print()
print('=' * 50)
print()
# MediaPipe初期化(Python SolutionsはCPU実行が標準である)
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
static_image_mode=False,
max_num_faces=1,
refine_landmarks=True,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)
print('MediaPipe初期化完了(CPU実行)')
print()
# グローバル変数
vas_value = 50
left_iris_sizes = deque(maxlen=10) # 虹彩径の履歴
right_iris_sizes = deque(maxlen=10) # 虹彩径の履歴
normalized_left_sizes = deque(maxlen=10)
normalized_right_sizes = deque(maxlen=10)
baseline_left_iris_size = None # 基準虹彩径
baseline_right_iris_size = None # 基準虹彩径
results_data = []
last_ear = 1.0
blink_threshold = 0.2
last_blink_analysis_time = time.time()
blink_count_10sec = 0
running = True # プログラム実行フラグ
frame_count = 0
results_log = []
# グラフ用データ
time_data = deque(maxlen=60)
vas_data_graph = deque(maxlen=60) # 名前変更
iris_data = deque(maxlen=60) # 虹彩径データ
cv_data = deque(maxlen=60)
lr_diff_data = deque(maxlen=60)
blink_freq_data = []
blink_freq_time = []
# 分析用データ(全期間)
all_vas_data = []
all_iris_data = [] # 虹彩径データ
all_cv_data = []
all_lr_diff_data = []
all_blink_freq_data = []
all_time_stamps = []
# 日本語フォント設定(固定)
FONT_PATH = 'C:/Windows/Fonts/meiryo.ttc'
FONT_SIZE = 20
# 虹彩ランドマークインデックスの定数化
# 左眼の虹彩輪郭ランドマーク(正しいインデックス)
LEFT_IRIS_IDX = [468, 469, 470, 471, 472]
# 右眼の虹彩輪郭ランドマーク(正しいインデックス)
RIGHT_IRIS_IDX = [473, 474, 475, 476, 477]
def create_vas_window():
root = tk.Tk()
root.title('VAS入力')
root.geometry('400x100')
def update_vas(val):
global vas_value
vas_value = int(float(val))
def on_closing():
global running
running = False
root.quit()
scale = tk.Scale(root, from_=0, to=100, orient=tk.HORIZONTAL,
length=350, command=update_vas)
scale.set(50)
scale.pack(pady=20)
label = tk.Label(root, text='0: 全く問題なし 100: 最大の不快感')
label.pack()
root.protocol("WM_DELETE_WINDOW", on_closing)
return root
def compute_iris_size(indices, landmarks, image_width, image_height):
# 楕円フィッティングによる虹彩径の推定
points = []
for idx in indices:
if idx < len(landmarks):
x = int(landmarks[idx].x * image_width)
y = int(landmarks[idx].y * image_height)
points.append([x, y])
iris_size = 0
if len(points) >= 5:
points = np.array(points)
# 楕円フィッティングを試みる
try:
ellipse = cv2.fitEllipse(points)
# 楕円の長軸と短軸の平均を径とする
iris_size = (ellipse[1][0] + ellipse[1][1]) / 2
except:
# フィッティング失敗時は点間距離の平均を使用
distances = []
for i in range(len(points)):
for j in range(i+1, len(points)):
dist = np.linalg.norm(points[i] - points[j])
distances.append(dist)
iris_size = np.mean(distances) if distances else 0
return iris_size
def calculate_iris_size_bilateral(landmarks, image_width, image_height):
# 左眼の虹彩輪郭ランドマーク(正しいインデックス)
left_iris = LEFT_IRIS_IDX
# 右眼の虹彩輪郭ランドマーク(正しいインデックス)
right_iris = RIGHT_IRIS_IDX
# 左眼虹彩径計算(楕円フィッティングによる改良版)
left_iris_size = compute_iris_size(left_iris, landmarks, image_width, image_height)
# 右眼虹彩径計算(楕円フィッティングによる改良版)
right_iris_size = compute_iris_size(right_iris, landmarks, image_width, image_height)
return left_iris_size, right_iris_size
def calculate_ear(landmarks):
# 左眼のランドマーク
left_eye = [33, 160, 158, 133, 153, 144]
points = []
for idx in left_eye:
if idx < len(landmarks):
points.append([landmarks[idx].x, landmarks[idx].y])
if len(points) == 6:
points = np.array(points)
# 垂直距離
vertical_1 = np.linalg.norm(points[1] - points[5])
vertical_2 = np.linalg.norm(points[2] - points[4])
# 水平距離
horizontal = np.linalg.norm(points[0] - points[3])
if horizontal > 0:
ear = (vertical_1 + vertical_2) / (2.0 * horizontal)
return ear
return 1.0
def video_frame_processing(frame):
global frame_count, vas_value, last_ear, blink_count_10sec
global left_iris_sizes, right_iris_sizes, normalized_left_sizes, normalized_right_sizes
global baseline_left_iris_size, baseline_right_iris_size
global last_blink_analysis_time, results_data
global time_data, vas_data_graph, iris_data, cv_data, lr_diff_data
global blink_freq_data, blink_freq_time
global all_vas_data, all_iris_data, all_cv_data, all_lr_diff_data
global all_blink_freq_data, all_time_stamps
current_time = time.time()
frame_count += 1
# BGR to RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb_frame)
display_frame = frame.copy()
result_text = f"フレーム{frame_count}: "
if results.multi_face_landmarks:
face_landmarks = results.multi_face_landmarks[0]
# 左右虹彩径計算
left_size, right_size = calculate_iris_size_bilateral(
face_landmarks.landmark,
frame.shape[1],
frame.shape[0]
)
# EAR計算と瞬き検出
current_ear = calculate_ear(face_landmarks.landmark)
# 瞬き検出
if last_ear > blink_threshold and current_ear <= blink_threshold:
blink_count_10sec += 1
result_text += f"瞬き検出 "
last_ear = current_ear
if left_size > 0 and right_size > 0:
left_iris_sizes.append(left_size)
right_iris_sizes.append(right_size)
# ベースライン設定
if baseline_left_iris_size is None and len(left_iris_sizes) >= 10:
baseline_left_iris_size = np.mean(left_iris_sizes)
baseline_right_iris_size = np.mean(right_iris_sizes)
# 正規化と分析
if baseline_left_iris_size and baseline_right_iris_size:
normalized_left = (left_size / baseline_left_iris_size) * 100
normalized_right = (right_size / baseline_right_iris_size) * 100
normalized_left_sizes.append(normalized_left)
normalized_right_sizes.append(normalized_right)
# 平均虹彩径と変動係数
avg_normalized = (normalized_left + normalized_right) / 2
if len(normalized_left_sizes) >= 5:
all_normalized = list(normalized_left_sizes) + list(normalized_right_sizes)
cv_value = np.std(all_normalized) / np.mean(all_normalized) * 100
else:
cv_value = 0
# 左右差計算(絶対値)
if (normalized_left + normalized_right) > 0:
lr_diff = abs(normalized_left - normalized_right) / ((normalized_left + normalized_right) / 2) * 100
if lr_diff <= 5:
lr_status = '理想的'
elif lr_diff <= 10:
lr_status = '軽度非対称'
else:
lr_status = '要確認'
else:
lr_diff = 0
lr_status = '計測中'
# ランドマークをプロット
# 左眼虹彩ランドマーク(青色)
left_iris_landmarks = LEFT_IRIS_IDX
for idx in left_iris_landmarks:
if idx < len(face_landmarks.landmark):
x = int(face_landmarks.landmark[idx].x * frame.shape[1])
y = int(face_landmarks.landmark[idx].y * frame.shape[0])
cv2.circle(display_frame, (x, y), 8, (255, 0, 0), -1)
# 右眼虹彩ランドマーク(赤色)
right_iris_landmarks = RIGHT_IRIS_IDX
for idx in right_iris_landmarks:
if idx < len(face_landmarks.landmark):
x = int(face_landmarks.landmark[idx].x * frame.shape[1])
y = int(face_landmarks.landmark[idx].y * frame.shape[0])
cv2.circle(display_frame, (x, y), 8, (0, 0, 255), -1)
# 瞬き検出用ランドマーク(緑色)
blink_landmarks = [33, 160, 158, 133, 153, 144]
for idx in blink_landmarks:
if idx < len(face_landmarks.landmark):
x = int(face_landmarks.landmark[idx].x * frame.shape[1])
y = int(face_landmarks.landmark[idx].y * frame.shape[0])
cv2.circle(display_frame, (x, y), 5, (0, 255, 0), -1)
# 画面表示(Pillow+OpenCV)
img_pil = Image.fromarray(cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
draw.text((10, 30), f'VAS: {vas_value}', font=font, fill=(0, 255, 0))
draw.text((10, 60), f'虹彩径: {avg_normalized:.1f}%', font=font, fill=(0, 255, 0))
draw.text((10, 90), f'変動係数: {cv_value:.1f}%', font=font, fill=(0, 255, 0))
draw.text((10, 120), f'左右差: {lr_diff:.1f}% ({lr_status})', font=font, fill=(0, 255, 0))
draw.text((10, 150), f'青点:左眼虹彩 赤点:右眼虹彩 緑点:瞬き検出', font=font, fill=(255, 255, 255))
display_frame = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
result_text += f"VAS:{vas_value} 虹彩径:{avg_normalized:.1f}% CV:{cv_value:.1f}% 左右差:{lr_diff:.1f}%"
# データ保存
time_data.append(current_time)
vas_data_graph.append(vas_value)
iris_data.append(avg_normalized)
cv_data.append(cv_value)
lr_diff_data.append(lr_diff)
all_time_stamps.append(current_time)
all_vas_data.append(vas_value)
all_iris_data.append(avg_normalized)
all_cv_data.append(cv_value)
all_lr_diff_data.append(lr_diff)
# 10秒ごとの瞬き頻度分析
elapsed_time = current_time - last_blink_analysis_time
if elapsed_time >= 10.0:
# 実際の経過時間に基づいて分あたりに換算
blink_per_minute = (blink_count_10sec / elapsed_time) * 60
blink_freq_data.append(blink_per_minute)
blink_freq_time.append(current_time)
all_blink_freq_data.append(blink_per_minute)
result_text += f" 瞬き頻度:{blink_per_minute:.0f}回/分"
blink_count_10sec = 0
last_blink_analysis_time = current_time
else:
result_text += "顔検出失敗"
# グラフ更新
update_graphs()
return display_frame, result_text, current_time
# グラフ初期化
plt.ion()
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 8))
fig.suptitle('心理生理状態リアルタイムモニタリング', fontsize=16)
# グラフ1: 主観評価と虹彩径
ax1.set_xlabel('時間(秒)')
ax1.set_ylabel('値')
ax1.set_title('主観評価と虹彩径の時系列変化')
ax1.set_ylim(0, 150)
line1_vas, = ax1.plot([], [], 'b-', label='VAS値')
line1_iris, = ax1.plot([], [], 'r-', label='虹彩径(%)')
ax1.legend()
ax1.text(0.5, -0.15, '青線:主観的不快感、赤線:虹彩径の相対値',
transform=ax1.transAxes, ha='center', fontsize=10)
# グラフ2: 変動指標
ax2.set_xlabel('時間(秒)')
ax2.set_ylabel('値')
ax2.set_title('変動指標')
ax2.set_ylim(0, 50)
line2_cv, = ax2.plot([], [], 'g-', label='変動係数(%)')
ax2_twin = ax2.twinx()
ax2_twin.set_ylabel('瞬き頻度(回/分)')
ax2_twin.set_ylim(0, 60)
line2_blink, = ax2_twin.plot([], [], 'mo', markersize=8, label='瞬き頻度')
ax2.legend(loc='upper left')
ax2_twin.legend(loc='upper right')
ax2.text(0.5, -0.15, '緑線:虹彩径の変動性、紫点:10秒ごとの瞬き頻度',
transform=ax2.transAxes, ha='center', fontsize=10)
# グラフ3: 左右対称性
ax3.set_xlabel('時間(秒)')
ax3.set_ylabel('左右差(%)')
ax3.set_title('虹彩径左右差の時系列変化')
ax3.set_ylim(0, 30)
line3, = ax3.plot([], [], 'orange')
ax3.axhline(y=5, color='green', linestyle='--', alpha=0.5, label='理想範囲')
ax3.axhline(y=10, color='red', linestyle='--', alpha=0.5, label='要確認')
ax3.legend()
ax3.text(0.5, -0.15, '5%以内が理想的、10%超は測定条件要確認',
transform=ax3.transAxes, ha='center', fontsize=10)
# グラフ4: 瞬き頻度トレンド
ax4.set_xlabel('時間(秒)')
ax4.set_ylabel('瞬き頻度(回/分)')
ax4.set_title('瞬き頻度の推移')
ax4.set_ylim(0, 60)
line4, = ax4.plot([], [], 'bo-', markersize=6)
ax4.axhspan(15, 20, alpha=0.2, color='green', label='正常範囲')
ax4.legend()
ax4.text(0.5, -0.15, '15-20回/分が正常、10回未満は疲労、30回超はストレス',
transform=ax4.transAxes, ha='center', fontsize=10)
plt.tight_layout()
def update_graphs():
try:
if len(time_data) > 0:
time_array = np.array(time_data) - time_data[0]
# グラフ1更新
line1_vas.set_data(time_array, vas_data_graph)
line1_iris.set_data(time_array, iris_data)
ax1.set_xlim(max(0, time_array[-1] - 60), time_array[-1] + 1)
# グラフ2更新
line2_cv.set_data(time_array, cv_data)
if len(blink_freq_time) > 0:
blink_time_array = np.array(blink_freq_time) - time_data[0]
line2_blink.set_data(blink_time_array, blink_freq_data)
ax2.set_xlim(max(0, time_array[-1] - 60), time_array[-1] + 1)
ax2_twin.set_xlim(max(0, time_array[-1] - 60), time_array[-1] + 1)
# グラフ3更新
line3.set_data(time_array, lr_diff_data)
ax3.set_xlim(max(0, time_array[-1] - 60), time_array[-1] + 1)
# グラフ4更新
if len(blink_freq_time) > 0:
blink_time_array = np.array(blink_freq_time) - time_data[0]
line4.set_data(blink_time_array, blink_freq_data)
ax4.set_xlim(max(0, time_array[-1] - 60), time_array[-1] + 1)
plt.draw()
plt.pause(0.001)
except:
pass
def calculate_correlations():
print('\n===== データ分析結果 =====')
print(f'測定時間: {len(all_time_stamps)}秒')
print(f'データ点数: {len(all_vas_data)}個')
print()
print('【相関の解釈基準】')
print('|r| ≥ 0.7: 強い相関')
print('0.4 ≤ |r| < 0.7: 中程度の相関')
print('0.2 ≤ |r| < 0.4: 弱い相関')
print('|r| < 0.2: ほぼ相関なし')
print('p < 0.05: 統計的に有意')
print()
print('【相関分析結果】')
correlation_results = []
correlation_results.append('===== データ分析結果 =====')
correlation_results.append(f'測定時間: {len(all_time_stamps)}秒')
correlation_results.append(f'データ点数: {len(all_vas_data)}個')
correlation_results.append('')
correlation_results.append('【相関の解釈基準】')
correlation_results.append('|r| ≥ 0.7: 強い相関')
correlation_results.append('0.4 ≤ |r| < 0.7: 中程度の相関')
correlation_results.append('0.2 ≤ |r| < 0.4: 弱い相関')
correlation_results.append('|r| < 0.2: ほぼ相関なし')
correlation_results.append('p < 0.05: 統計的に有意')
correlation_results.append('')
correlation_results.append('【相関分析結果】')
# 相関分析の実行
if len(all_vas_data) > 2 and len(all_iris_data) > 2:
r1, p1 = stats.pearsonr(all_vas_data, all_iris_data)
interpretation1 = interpret_correlation(r1)
print(f'1. VAS値 × 平均虹彩径')
print(f' 相関係数: {r1:.3f} (p値: {p1:.3f})')
print(f' 解釈: {interpretation1}')
print()
correlation_results.append(f'1. VAS値 × 平均虹彩径')
correlation_results.append(f' 相関係数: {r1:.3f} (p値: {p1:.3f})')
correlation_results.append(f' 解釈: {interpretation1}')
correlation_results.append('')
if len(all_vas_data) > 2 and len(all_cv_data) > 2:
r2, p2 = stats.pearsonr(all_vas_data, all_cv_data)
interpretation2 = interpret_correlation(r2)
print(f'2. VAS値 × 虹彩径変動係数')
print(f' 相関係数: {r2:.3f} (p値: {p2:.3f})')
print(f' 解釈: {interpretation2}')
print()
correlation_results.append(f'2. VAS値 × 虹彩径変動係数')
correlation_results.append(f' 相関係数: {r2:.3f} (p値: {p2:.3f})')
correlation_results.append(f' 解釈: {interpretation2}')
correlation_results.append('')
if len(blink_freq_time) > 2 and len(all_vas_data) > 2:
vas_at_blink_times = np.interp(blink_freq_time, all_time_stamps, all_vas_data)
min_len = min(len(vas_at_blink_times), len(all_blink_freq_data))
if min_len > 2:
r3, p3 = stats.pearsonr(vas_at_blink_times[:min_len], all_blink_freq_data[:min_len])
interpretation3 = interpret_correlation(r3)
print(f'3. VAS値 × 瞬き頻度')
print(f' 相関係数: {r3:.3f} (p値: {p3:.3f})')
print(f' 解釈: {interpretation3}')
print()
correlation_results.append(f'3. VAS値 × 瞬き頻度')
correlation_results.append(f' 相関係数: {r3:.3f} (p値: {p3:.3f})')
correlation_results.append(f' 解釈: {interpretation3}')
correlation_results.append('')
if len(blink_freq_time) > 2 and len(all_cv_data) > 2:
cv_at_blink_times = np.interp(blink_freq_time, all_time_stamps, all_cv_data)
min_len = min(len(cv_at_blink_times), len(all_blink_freq_data))
if min_len > 2:
r4, p4 = stats.pearsonr(cv_at_blink_times[:min_len], all_blink_freq_data[:min_len])
interpretation4 = interpret_correlation(r4)
print(f'4. 虹彩径変動係数 × 瞬き頻度')
print(f' 相関係数: {r4:.3f} (p値: {p4:.3f})')
print(f' 解釈: {interpretation4}')
print()
correlation_results.append(f'4. 虹彩径変動係数 × 瞬き頻度')
correlation_results.append(f' 相関係数: {r4:.3f} (p値: {p4:.3f})')
correlation_results.append(f' 解釈: {interpretation4}')
correlation_results.append('')
if len(all_vas_data) > 2 and len(all_lr_diff_data) > 2:
r5, p5 = stats.pearsonr(all_vas_data, all_lr_diff_data)
interpretation5 = interpret_correlation(r5)
print(f'5. VAS値 × 虹彩径左右差')
print(f' 相関係数: {r5:.3f} (p値: {p5:.3f})')
print(f' 解釈: {interpretation5}')
print()
correlation_results.append(f'5. VAS値 × 虹彩径左右差')
correlation_results.append(f' 相関係数: {r5:.3f} (p値: {p5:.3f})')
correlation_results.append(f' 解釈: {interpretation5}')
correlation_results.append('')
# 統計サマリー
print('【統計サマリー】')
correlation_results.append('【統計サマリー】')
if len(all_vas_data) > 0:
print(f'- VAS値: 平均 {np.mean(all_vas_data):.1f}, 標準偏差 {np.std(all_vas_data):.1f}, 範囲 {min(all_vas_data)}-{max(all_vas_data)}')
correlation_results.append(f'- VAS値: 平均 {np.mean(all_vas_data):.1f}, 標準偏差 {np.std(all_vas_data):.1f}, 範囲 {min(all_vas_data)}-{max(all_vas_data)}')
if len(all_iris_data) > 0:
print(f'- 虹彩径: 平均 {np.mean(all_iris_data):.1f}%, 標準偏差 {np.std(all_iris_data):.1f}%, 範囲 {min(all_iris_data):.1f}-{max(all_iris_data):.1f}%')
correlation_results.append(f'- 虹彩径: 平均 {np.mean(all_iris_data):.1f}%, 標準偏差 {np.std(all_iris_data):.1f}%, 範囲 {min(all_iris_data):.1f}-{max(all_iris_data):.1f}%')
if len(all_cv_data) > 0:
print(f'- 変動係数: 平均 {np.mean(all_cv_data):.1f}%, 標準偏差 {np.std(all_cv_data):.1f}%, 範囲 {min(all_cv_data):.1f}-{max(all_cv_data):.1f}%')
correlation_results.append(f'- 変動係数: 平均 {np.mean(all_cv_data):.1f}%, 標準偏差 {np.std(all_cv_data):.1f}%, 範囲 {min(all_cv_data):.1f}-{max(all_cv_data):.1f}%')
if len(all_blink_freq_data) > 0:
print(f'- 瞬き頻度: 平均 {np.mean(all_blink_freq_data):.1f}回/分, 標準偏差 {np.std(all_blink_freq_data):.1f}, 範囲 {min(all_blink_freq_data):.0f}-{max(all_blink_freq_data):.0f}回/分')
correlation_results.append(f'- 瞬き頻度: 平均 {np.mean(all_blink_freq_data):.1f}回/分, 標準偏差 {np.std(all_blink_freq_data):.1f}, 範囲 {min(all_blink_freq_data):.0f}-{max(all_blink_freq_data):.0f}回/分')
if len(all_lr_diff_data) > 0:
print(f'- 左右差: 平均 {np.mean(all_lr_diff_data):.1f}%, 標準偏差 {np.std(all_lr_diff_data):.1f}%, 最大 {max(all_lr_diff_data):.1f}%')
correlation_results.append(f'- 左右差: 平均 {np.mean(all_lr_diff_data):.1f}%, 標準偏差 {np.std(all_lr_diff_data):.1f}%, 最大 {max(all_lr_diff_data):.1f}%')
return correlation_results
def interpret_correlation(r):
abs_r = abs(r)
if abs_r >= 0.7:
strength = '強い'
elif abs_r >= 0.4:
strength = '中程度の'
elif abs_r >= 0.2:
strength = '弱い'
else:
return 'ほぼ相関なし'
direction = '正の' if r > 0 else '負の'
return f'{direction}{strength}相関あり'
def print_usage():
print('操作方法:')
print('- VASスライダーで主観的な状態を入力してください(0-100)')
print('- qキーで終了します')
print('- 画面表示: 青点=左眼虹彩、赤点=右眼虹彩、緑点=瞬き検出用ランドマーク')
print()
print_usage()
# VASウィンドウ作成
vas_window = create_vas_window()
# カメラ初期化
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not cap.isOpened():
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
if not cap.isOpened():
print('カメラの初期化に失敗しました')
exit()
# 計測開始時の説明表示
print('===== 計測システムの詳細 =====')
print()
print('【計測データ】')
print('本システムは以下の6種類のデータを同時計測します:')
print()
print('1. VAS(Visual Analog Scale)値')
print(' - 0-100の連続尺度による主観的評価')
print(' - スライダーで随時調整可能')
print()
print('2. 左右虹彩径')
print(' - MediaPipe Face Meshの虹彩ランドマーク(左眼:468-472、右眼:473-477)を使用')
print(' - 虹彩輪郭から径を計測')
print(' - 個人差補正のため初期10フレームで正規化')
print()
print('3. 虹彩径左右差')
print(' - (|左眼径-右眼径|)/平均径×100で算出')
print(' - 5%以内:理想的、5-10%:軽度非対称、10%超:要確認')
print()
print('4. 虹彩径変動係数(CV)')
print(' - 標準偏差/平均×100で算出')
print(' - 測定値の変動性を定量化')
print()
print('5. 瞬き検出')
print(' - Eye Aspect Ratio(EAR)による検出')
print(' - 発生時刻をミリ秒単位で記録')
print()
print('6. 瞬き頻度')
print(' - 10秒間隔で集計し分あたりに換算')
print(' - 15-20回/分が正常範囲')
print()
print('【計測原理】')
print('虹彩径の測定値の変動を分析し、')
print('VASとの相関分析により、主観と客観の関係を評価します。')
print()
print('注意:本システムは虹彩輪郭から径を計測しており、')
print('瞳孔径の直接測定ではありません。')
print()
print('計測を開始します...')
print('=====================================')
print()
print('\n=== 動画処理開始 ===')
print_usage()
try:
while running:
try:
vas_window.update_idletasks()
vas_window.update()
except:
running = False
break
ret, frame = cap.read()
if not ret:
break
MAIN_FUNC_DESC = "VAS・虹彩径・瞬き計測"
processed_frame, result, current_time = video_frame_processing(frame)
cv2.imshow(MAIN_FUNC_DESC, processed_frame)
print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], result)
results_log.append(result)
if cv2.waitKey(1) & 0xFF == ord('q'):
running = False
break
finally:
print('\n=== プログラム終了 ===')
cap.release()
cv2.destroyAllWindows()
try:
vas_window.destroy()
except:
pass
plt.close('all')
# 相関分析
correlation_results = calculate_correlations()
# 結果保存
if results_log:
with open('result.txt', 'w', encoding='utf-8') as f:
f.write('=== 結果 ===\n')
f.write(f'処理フレーム数: {frame_count}\n')
f.write(f'使用デバイス: CPU\n')
f.write('\n')
f.write('\n'.join(results_log))
f.write('\n\n')
for line in correlation_results:
f.write(line + '\n')
print(f'\n処理結果をresult.txtに保存しました')