Compare commits

..
33 Commits
Author SHA1 Message Date
weigmi87303 0702864bc3 add note to files on remote directory 2026-03-27 09:39:58 +01:00
weigmi87303 ac2c88c339 fixed grammar error in read me 2026-03-22 14:09:59 +00:00
korzerce84997 96b3e35248 changes to CNN report 2026-03-19 18:44:00 +01:00
korzerce84997 0483c3fea3 upload CNN report 2026-03-19 17:42:32 +01:00
TimoKurz 3701d11c77 - added accuracy results for xgboost models 2026-03-19 17:37:32 +01:00
TimoKurz 145a5ecf78 - added eyeFeature_kalibrierung
- added documentation for eyeTracking data
2026-03-19 16:20:42 +01:00
weigmi87303 eba9b07487 fixed typos and added clickable links to doc files 2026-03-18 12:29:44 +01:00
TimoKurz 4df1187f84 - added notes for XGBoost without accuracy details
- deleted unused files for data_creation and modified the project_report file overview
- translated the documentation for the pyfeat implementation
2026-03-14 14:33:35 +01:00
TimoKurz c439e35e39 -- added correct versioning for feature extraction 2026-03-14 13:41:58 +01:00
weigmi87303 9406be3c18 models vorlaeufig fertig 2026-03-10 21:29:04 +01:00
weigmi87303 f344808802 wrote chapter Isolation forest 2026-03-10 19:38:00 +01:00
weigmi87303 10fdafa244 added prediction env 2026-03-10 19:17:08 +01:00
weigmi87303 314c4433d3 general information in model training 2026-03-10 16:16:46 +01:00
weigmi87303 b252082991 chapter EDA written 2026-03-10 15:09:51 +01:00
weigmi87303 2ec0af5f62 2.1 and 2.2 written 2026-03-10 14:23:02 +01:00
weigmi87303 9c2619daa9 crash 2026-03-10 13:11:03 +01:00
weigmi87303 910e642398 mini readme change 2026-03-10 12:03:59 +01:00
weigmi87303 0e6f39556b changed chapter numbers 2026-03-10 11:48:50 +01:00
weigmi87303 fb0d39c668 cleaned readme, added project report 2026-03-09 20:45:24 +01:00
weigmi87303 0b2c629d16 changed owncloud download notebook 2026-03-09 20:25:54 +01:00
weigmi87303 ef785283f0 updatet subject performance notebook 2026-03-09 20:10:55 +01:00
weigmi87303 182fc102de clean up in eda distribution plots 2026-03-05 13:41:50 +01:00
weigmi87303 a064f6cc90 outsourcing of functions 2026-03-05 13:38:04 +01:00
weigmi87303 a4b7190756 first draft for read me + requirements.txt 2026-03-05 13:18:17 +01:00
weigmi87303 f95d59e44d removed dataset file from repo 2026-03-05 12:16:52 +01:00
weigmi87303 de12c1407c current files from jetson board 2026-03-05 12:13:27 +01:00
weigmi87303 537b452449 changed leaky relu syntax to remove keras bug 2026-03-04 17:59:05 +01:00
weigmi87303 3169c29319 mini changes in predict pipeline 2026-03-04 17:01:43 +01:00
weigmi87303 13bd76631f outsourcing of scaler in iforest and deep svdd, removal of paths 2026-03-04 16:51:22 +01:00
weigmi87303 6cc38291df clean up predict pipeline 1 2026-03-04 16:21:06 +01:00
weigmi87303 8b6c547387 deletion of vae files 2026-03-04 15:19:15 +01:00
weigmi87303 de0084dc09 getting rid of redundant files in dataset creation 2026-03-04 15:09:23 +01:00
weigmi87303 af3f9d16b2 minor fixes to new paths / dataset with all columns 2026-03-04 12:25:07 +01:00
43 changed files with 1917 additions and 5394 deletions
+2 -1
View File
@@ -7,4 +7,5 @@
!.gitignore
!*.service
!*.timer
!*.yaml
!*.yaml
!*.txt
+3 -4
View File
@@ -23,7 +23,7 @@
"metadata": {},
"outputs": [],
"source": [
"file_path = \"adabase-public-0020-v_0_0_2.h5py\""
"file_path = \"YOUR_FILE_PATH.h5py\""
]
},
{
@@ -87,7 +87,7 @@
"id": "a4731c56",
"metadata": {},
"source": [
"Actions units"
"Insights on actions units"
]
},
{
@@ -167,7 +167,7 @@
"id": "332740a8",
"metadata": {},
"source": [
"Plots"
"Example plot of ECG curve"
]
},
{
@@ -177,7 +177,6 @@
"metadata": {},
"outputs": [],
"source": [
"# df_signals_ecg = pd.read_hdf(file_path, \"SIGNALS\", mode=\"r\", columns=[\"STUDY\",\"LEVEL\", \"PHASE\", 'RAW_ECG_I'])\n",
"df_signals_ecg = df_signals[[\"STUDY\",\"LEVEL\", \"PHASE\", 'RAW_ECG_I']]\n",
"df_signals_ecg.shape"
]
+1 -3
View File
@@ -37,9 +37,7 @@
"metadata": {},
"outputs": [],
"source": [
"dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/combined_dataset_25hz.parquet\")\n",
"# dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/60s_combined_dataset_25hz.parquet\")\n",
"# dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/120s_combined_dataset_25hz.parquet\")"
"dataset_path = Path(r\"\") # TODO: enter path to dataset"
]
},
{
+55 -22
View File
@@ -1,5 +1,13 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "cc08936c",
"metadata": {},
"source": [
"## Insights into the dataset with histogramms and scatter plots"
]
},
{
"cell_type": "markdown",
"id": "1014c5e0",
@@ -17,7 +25,8 @@
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt"
"import matplotlib.pyplot as plt\n",
"from pathlib import Path"
]
},
{
@@ -27,7 +36,7 @@
"metadata": {},
"outputs": [],
"source": [
"path =r\"C:\\Users\\micha\\FAUbox\\WS2526_Fahrsimulator_MSY (Celina Korzer)\\AU_dataset\\output_windowed.parquet\"\n",
"path = Path(r\".parquet\") # TODO: enter path to dataset\n",
"df = pd.read_parquet(path=path)"
]
},
@@ -104,21 +113,27 @@
"metadata": {},
"outputs": [],
"source": [
"# Get all columns that start with 'AU'\n",
"au_columns = [col for col in low_all.columns if col.startswith('AU')]\n",
"face_au_cols = [c for c in low_all.columns if c.startswith(\"FACE_AU\")]\n",
"eye_cols = ['Fix_count_short_66_150', 'Fix_count_medium_300_500',\n",
" 'Fix_count_long_gt_1000', 'Fix_count_100', 'Fix_mean_duration',\n",
" 'Fix_median_duration', 'Sac_count', 'Sac_mean_amp', 'Sac_mean_dur',\n",
" 'Sac_median_dur', 'Blink_count', 'Blink_mean_dur', 'Blink_median_dur',\n",
" 'Pupil_mean', 'Pupil_IPA']\n",
"\n",
"cols = face_au_cols+eye_cols\n",
"\n",
"# Calculate number of rows and columns for subplots\n",
"n_cols = len(au_columns)\n",
"n_rows = 4\n",
"n_cols = len(cols)\n",
"n_rows = 7\n",
"n_cols_subplot = 5\n",
"\n",
"# Create figure with subplots\n",
"fig, axes = plt.subplots(n_rows, n_cols_subplot, figsize=(20, 16))\n",
"axes = axes.flatten()\n",
"fig.suptitle('Action Unit (AU) Distributions: Low vs High', fontsize=20, fontweight='bold', y=0.995)\n",
"fig.suptitle('Feature Distributions: Low vs High', fontsize=20, fontweight='bold', y=0.995)\n",
"\n",
"# Create histogram for each AU column\n",
"for idx, col in enumerate(au_columns):\n",
"for idx, col in enumerate(cols):\n",
" ax = axes[idx]\n",
" \n",
" # Plot overlapping histograms\n",
@@ -133,32 +148,50 @@
" ax.grid(True, alpha=0.3)\n",
"\n",
"# Hide any unused subplots\n",
"for idx in range(len(au_columns), len(axes)):\n",
"for idx in range(len(cols), len(axes)):\n",
" axes[idx].set_visible(False)\n",
"\n",
"# Adjust layout\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6cd53cdb",
"metadata": {},
"outputs": [],
"source": [
"# Create figure with subplots\n",
"fig, axes = plt.subplots(n_rows, n_cols_subplot, figsize=(20, 16))\n",
"axes = axes.flatten()\n",
"fig.suptitle('Feature Scatter: Low vs High', fontsize=20, fontweight='bold', y=0.995)\n",
"\n",
"for idx, col in enumerate(cols):\n",
" ax = axes[idx]\n",
"\n",
" # Scatterplots\n",
" ax.scatter(range(len(low_all[col])), low_all[col], alpha=0.6, color='blue', label='low_all', s=10)\n",
" ax.scatter(range(len(high_all[col])), high_all[col], alpha=0.6, color='red', label='high_all', s=10)\n",
"\n",
" ax.set_title(col, fontsize=10, fontweight='bold')\n",
" ax.set_xlabel('Sample index', fontsize=8)\n",
" ax.set_ylabel('Value', fontsize=8)\n",
" ax.legend(fontsize=8)\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
"\n",
"plt.tight_layout()\n",
"plt.show()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
}
},
"nbformat": 4,
+2
View File
@@ -0,0 +1,2 @@
- url: # enter url
- password: # enter passwort
-157
View File
@@ -1,157 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "aab6b326-a583-47ad-8bb7-723c2fddcc63",
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"# %pip install pyocclient\n",
"import yaml\n",
"import owncloud\n",
"import pandas as pd\n",
"import time"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4f42846c-27c3-4394-a40a-e22d73c2902e",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"\n",
"with open(\"../login.yaml\") as f:\n",
" cfg = yaml.safe_load(f)\n",
"url, password = cfg[0][\"url\"], cfg[1][\"password\"]\n",
"file = \"adabase-public-0022-v_0_0_2.h5py\"\n",
"oc = owncloud.Client.from_public_link(url, folder_password=password)\n",
"\n",
"\n",
"oc.get_file(file, \"tmp22.h5\")\n",
"\n",
"end = time.time()\n",
"print(end - start)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3714dec2-85d0-4f76-af46-ea45ebec2fa3",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"df_performance = pd.read_hdf(\"tmp22.h5\", \"PERFORMANCE\")\n",
"end = time.time()\n",
"print(end - start)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f50e97d0",
"metadata": {},
"outputs": [],
"source": [
"print(22)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c131c816",
"metadata": {},
"outputs": [],
"source": [
"df_performance"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6ae47e52-ad86-4f8d-b929-0080dc99f646",
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"df_4_col = pd.read_hdf(\"tmp.h5\", \"SIGNALS\", mode=\"r\", columns=[\"STUDY\"], start=0, stop=1)\n",
"end = time.time()\n",
"print(end - start)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7c139f3a-ede8-4530-957d-d1bb939f6cb5",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a68d58ea-65f2-46c4-a2b2-8c3447c715d7",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "95aa4523-3784-4ab6-bf92-0227ce60e863",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.info()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "defbcaf4-ad1b-453f-9b48-ab0ecfc4b5d5",
"metadata": {},
"outputs": [],
"source": [
"df_4_col.isna().sum()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "72313895-c478-44a5-9108-00b0bec01bb8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+112
View File
@@ -0,0 +1,112 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "457e7807",
"metadata": {},
"source": [
"# Get data from owncloud"
]
},
{
"cell_type": "markdown",
"id": "dc9ed3f8",
"metadata": {},
"source": [
"Imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# %pip install pyocclient\n",
"import os\n",
"import time\n",
"import yaml\n",
"import owncloud\n",
"import pandas as pd\n"
]
},
{
"cell_type": "markdown",
"id": "68e34abc",
"metadata": {},
"source": [
"Download and save"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"start = time.time()\n",
"\n",
"# TODO: User input: directory where downloaded files should be saved\n",
"save_dir = r\"./downloads\"\n",
"\n",
"\n",
"os.makedirs(save_dir, exist_ok=True)\n",
"\n",
"# Load credentials\n",
"with open(\"login.yaml\", \"r\") as f:\n",
" cfg = yaml.safe_load(f)\n",
"\n",
"url = cfg[0][\"url\"]\n",
"password = cfg[1][\"password\"]\n",
"\n",
"# Connect to OwnCloud public link\n",
"oc = owncloud.Client.from_public_link(url, folder_password=password)\n",
"\n",
"# List all available files in the shared folder\n",
"remote_files = oc.list(\".\")\n",
"\n",
"# Keep only HDF5 files and sort them by name\n",
"hdf5_files = sorted([f.get_name() for f in remote_files if f.get_name().endswith(\".hdf5\")])\n",
"\n",
"print(f\"Found {len(hdf5_files)} .hdf5 files in OwnCloud\")\n",
"\n",
"if not hdf5_files:\n",
" print(\"No .hdf5 files found.\")\n",
"else:\n",
" for i, remote_name in enumerate(hdf5_files):\n",
" local_name = f\"tmp_{i:04d}.h5\"\n",
" local_path = os.path.join(save_dir, local_name)\n",
"\n",
" try:\n",
" oc.get_file(remote_name, local_path)\n",
" print(f\"Downloaded: {remote_name} -> {local_path}\")\n",
" except Exception as e:\n",
" print(f\"Failed to download {remote_name}: {e}\")\n",
"\n",
"end = time.time()\n",
"print(f\"Finished in {end - start:.2f} seconds\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+68 -104
View File
@@ -15,6 +15,7 @@
"metadata": {},
"outputs": [],
"source": [
"%pip install pyocclient\n",
"import yaml\n",
"import owncloud\n",
"import pandas as pd\n",
@@ -36,101 +37,109 @@
"metadata": {},
"outputs": [],
"source": [
"# Load credentials\n",
"with open(\"../login.yaml\") as f:\n",
"# Load credentials from YAML\n",
"with open(\"login.yaml\", \"r\") as f:\n",
" cfg = yaml.safe_load(f)\n",
" \n",
"url, password = cfg[0][\"url\"], cfg[1][\"password\"]\n",
"\n",
"# Connect once\n",
"url = cfg[0][\"url\"]\n",
"password = cfg[1][\"password\"]\n",
"\n",
"# Connect once to the public OwnCloud link\n",
"oc = owncloud.Client.from_public_link(url, folder_password=password)\n",
"# File pattern\n",
"# base = \"adabase-public-{num:04d}-v_0_0_2.h5py\"\n",
"base = \"{num:04d}-*.h5py\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "07c03d07",
"metadata": {},
"outputs": [],
"source": [
"num_files = 2 # number of files to process (min: 1, max: 30)\n",
"\n",
"num_files = 1 # number of subject IDs to process (min: 1, max: 30)\n",
"performance_data = []\n",
"\n",
"# Read remote file list once\n",
"remote_files = oc.list(\".\")\n",
"remote_names = [f.get_name() for f in remote_files]\n",
"\n",
"for i in range(num_files):\n",
" file_pattern = f\"{i:04d}-*\"\n",
" \n",
" # Get list of files matching the pattern\n",
" files = oc.list('.')\n",
" matching_files = [f.get_name() for f in files if f.get_name().startswith(f\"{i:04d}-\")]\n",
" \n",
" if matching_files:\n",
" file_name = matching_files[0] # Take the first matching file\n",
" local_tmp = f\"tmp_{i:04d}.h5\"\n",
" \n",
" oc.get_file(file_name, local_tmp)\n",
" print(f\"{file_name} geöffnet\")\n",
" else:\n",
" print(f\"Keine Datei gefunden für Muster: {file_pattern}\")\n",
" # file_name = base.format(num=i)\n",
" # local_tmp = f\"tmp_{i:04d}.h5\"\n",
" prefix = f\"{i:04d}-\"\n",
" matching_files = [name for name in remote_names if name.startswith(prefix) and name.endswith(\".hdf5\")]\n",
"\n",
" # oc.get_file(file_name, local_tmp)\n",
" # print(f\"{file_name} geöffnet\")\n",
"\n",
" # check SIGNALS table for AUs\n",
" with pd.HDFStore(local_tmp, mode=\"r\") as store:\n",
" cols = store.select(\"SIGNALS\", start=0, stop=1).columns\n",
" au_cols = [c for c in cols if c.startswith(\"AU\")]\n",
" if not au_cols:\n",
" print(f\"Subject {i} enthält keine AUs\")\n",
" if not matching_files:\n",
" print(f\"No file found for pattern: {prefix}*.hdf5\")\n",
" continue\n",
"\n",
" # load performance table\n",
" with pd.HDFStore(local_tmp, mode=\"r\") as store:\n",
" perf_df = store.select(\"PERFORMANCE\")\n",
" # Take the first matching file, e.g. 0000-AACA.hdf5\n",
" file_name = matching_files[0]\n",
" local_tmp = f\"tmp_{i:04d}.hdf5\"\n",
"\n",
" try:\n",
" # Download the file locally\n",
" oc.get_file(file_name, local_tmp)\n",
" print(f\"Downloaded and opened file: {file_name} -> {local_tmp}\")\n",
" except Exception as e:\n",
" print(f\"Failed to download file {file_name}: {e}\")\n",
" continue\n",
"\n",
" # Check SIGNALS table for AU columns\n",
" try:\n",
" with pd.HDFStore(local_tmp, mode=\"r\") as store:\n",
" cols = store.select(\"SIGNALS\", start=0, stop=1).columns\n",
" except Exception as e:\n",
" print(f\"Failed to read SIGNALS from {local_tmp}: {e}\")\n",
" continue\n",
"\n",
" au_cols = [c for c in cols if c.startswith(\"AU\")]\n",
" if not au_cols:\n",
" print(f\"Subject {i:04d} contains no AU columns\")\n",
" continue\n",
"\n",
" # Load PERFORMANCE table\n",
" try:\n",
" with pd.HDFStore(local_tmp, mode=\"r\") as store:\n",
" perf_df = store.select(\"PERFORMANCE\")\n",
" except Exception as e:\n",
" print(f\"Failed to read PERFORMANCE from {local_tmp}: {e}\")\n",
" continue\n",
"\n",
" f1_cols = [c for c in [\"AUDITIVE F1\", \"VISUAL F1\", \"F1\"] if c in perf_df.columns]\n",
" if not f1_cols:\n",
" print(f\"Subject {i}: keine F1-Spalten gefunden\")\n",
" print(f\"Subject {i:04d}: no F1 columns found\")\n",
" continue\n",
"\n",
" subject_entry = {\"subjectID\": i}\n",
" valid_scores = []\n",
"\n",
" # iterate rows: each (study, level, phase)\n",
" # Iterate through PERFORMANCE rows: each row is one (study, level, phase) combination\n",
" for _, row in perf_df.iterrows():\n",
" study, level, phase = row[\"STUDY\"], row[\"LEVEL\"], row[\"PHASE\"]\n",
" study = row[\"STUDY\"]\n",
" level = row[\"LEVEL\"]\n",
" phase = row[\"PHASE\"]\n",
" col_name = f\"STUDY_{study}_LEVEL_{level}_PHASE_{phase}\"\n",
"\n",
" # collect valid F1 values among the three columns\n",
" # Collect non-NaN F1 values from the available F1 columns\n",
" scores = [row[c] for c in f1_cols if pd.notna(row[c])]\n",
" if scores:\n",
" mean_score = float(np.mean(scores))\n",
" subject_entry[col_name] = mean_score\n",
" valid_scores.extend(scores)\n",
"\n",
" # compute overall average across all valid combinations\n",
" # Compute overall average across all valid F1 values\n",
" if valid_scores:\n",
" subject_entry[\"overall_score\"] = float(np.mean(valid_scores))\n",
" performance_data.append(subject_entry)\n",
" print(f\"Subject {i}: {len(valid_scores)} gültige Scores, Overall = {subject_entry['overall_score']:.3f}\")\n",
" print(\n",
" f\"Subject {i:04d}: {len(valid_scores)} valid scores, \"\n",
" f\"overall = {subject_entry['overall_score']:.3f}\"\n",
" )\n",
" else:\n",
" print(f\"Subject {i}: keine gültigen F1-Scores\")\n",
" print(f\"Subject {i:04d}: no valid F1 scores found\")\n",
"\n",
"# build dataframe\n",
"# Build final DataFrame and save CSV\n",
"if performance_data:\n",
" performance_df = pd.DataFrame(performance_data)\n",
" combination_cols = sorted([c for c in performance_df.columns if c.startswith(\"STUDY_\")])\n",
" final_cols = [\"subjectID\", \"overall_score\"] + combination_cols\n",
" performance_df = performance_df[final_cols]\n",
" performance_df.to_csv(\"n_au_performance.csv\", index=False)\n",
" performance_df.to_csv(\"performance.csv\", index=False)\n",
"\n",
" print(f\"\\nGesamt Subjects mit Action Units: {len(performance_df)}\")\n",
" print(f\"\\nTotal subjects with Action Units: {len(performance_df)}\")\n",
" print(\"Saved results to performance.csv\")\n",
"else:\n",
" print(\"Keine gültigen Daten gefunden.\")"
" print(\"No valid data found.\")"
]
},
{
@@ -142,56 +151,11 @@
"source": [
"performance_df.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "db95eea7",
"metadata": {},
"outputs": [],
"source": [
"with pd.HDFStore(\"tmp_0000.h5\", mode=\"r\") as store:\n",
" md = store.select(\"META\")\n",
"print(\"File 0:\")\n",
"print(md)\n",
"with pd.HDFStore(\"tmp_0001.h5\", mode=\"r\") as store:\n",
" md = store.select(\"META\")\n",
"print(\"File 1\")\n",
"print(md)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8067036b",
"metadata": {},
"outputs": [],
"source": [
"pd.set_option('display.max_columns', None)\n",
"pd.set_option('display.max_rows', None)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f18e7385",
"metadata": {},
"outputs": [],
"source": [
"with pd.HDFStore(\"tmp_0000.h5\", mode=\"r\") as store:\n",
" md = store.select(\"SIGNALS\", start=0, stop=1)\n",
"print(\"File 0:\")\n",
"md.head()\n",
"# with pd.HDFStore(\"tmp_0001.h5\", mode=\"r\",start=0, stop=1) as store:\n",
"# md = store.select(\"SIGNALS\")\n",
"# print(\"File 1\")\n",
"# print(md.columns)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"display_name": "310",
"language": "python",
"name": "python3"
},
@@ -205,7 +169,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
"version": "3.10.19"
}
},
"nbformat": 4,
@@ -1,58 +0,0 @@
from feat import Detector
from feat.utils.io import get_test_data_path
from moviepy.video.io.VideoFileClip import VideoFileClip
import os
def extract_aus(path, model, skip_frames):
detector = Detector(au_model=model)
video_prediction = detector.detect(
path, data_type="video", skip_frames=skip_frames, face_detection_threshold=0.95 # alle 5 Sekunden einbeziehen - 24 Frames pro Sekunde
)
return video_prediction.aus.sum()
def split_video(path, chunk_length=120):
video = VideoFileClip(path)
duration = int(video.duration)
subclips_dir = os.path.join(os.dirname(path), "subclips")
os.makedirs(subclips_dir, exist_ok=True)
paths = []
for start in range(0, duration, chunk_length):
end = min(start + chunk_length, duration)
subclip = (
video
.subclip(start, end)
.without_audio()
.set_fps(video.fps)
)
output_path = f"{subclips_dir}_part_{start//chunk_length + 1}.mp4"
subclip.write_videofile(
output_path,
)
paths.append(output_path)
return output_path
# def start(path):
# results = []
# clips = split_video(path)
# for clip in clips:
# results.append(extract_aus(clip, 'svm', 25*5))
# return results
if __name__ == "__main__":
results = []
clips = []
test_video_path = "AU_creation/YTDown.com_YouTube_Was-ist-los-bei-7-vs-Wild_Media_Gtj9zu_WikU_001_1080p.mp4"
clips = split_video(test_video_path)
for clippath in clips:
results.append(extract_aus(clippath, 'svm', 25*5))
print(results)
+39 -19
View File
@@ -5,27 +5,47 @@
"id": "3b0c6c82",
"metadata": {},
"source": [
"Hier entsteht die Dokumentation, wie die Action Units erzeugt wurden.\n",
"Daraus wird dann letztendlich ein Skript erstellt, welches automatisch AUs aus Videodateien erstellen soll.\n",
"## Action Unit Documentation and Setup\n",
"\n",
"Py-Feat besitzt Dependencies, die ab Python 3.12 nicht mehr verfügbar sind.\n",
"Dazu muss ein Kernel mit Python 3.11 erstellt werden.\n",
"Folgendes Vorgehen:\n",
"1. Seite des Jupyter Labs öffnen\n",
"2. Terminal öffnen und folgende Befehle eingeben:\n",
" conda create -n py311 python=3.11\n",
" source ~/.bashrc\n",
" conda activate py311\n",
" conda install jupyter\n",
" python -m ipykernel install --user --name=py311 --display-name \"Python 3.11\"\n",
" pip install py-feat\n",
" pip install \"moviepy<2.0\" (falls benötigt)\n",
"3. den Kernel neustarten\n",
"4. in VSC den Kernel neu hinzufügen und dann den Kernel mit dem Namen \"Python 3.11\" auswählen.\n",
"This documentation outlines the process for generating **Action Units (AUs)** and the eventual creation of a script to automate AU extraction from video files.\n",
"\n",
"Der Code unten zeigt eine beispielhafte Integration der py-feat Bibliothek.\n",
"Die Klassifizierung zu 0,1 kommt durch die Wahl des AU-Modells zustande. Dabei wird SVM gewählt. (ADABase Paper)\n",
"Gibt die Klassifizierung einen Gleitkommawert zwischen 0 & 1 aus, dann kommt XGB zum Einsatz. (REVELIO Paper)"
"### Python Environment Configuration\n",
"\n",
"**Py-Feat** relies on dependencies that are incompatible with Python 3.12 and later. To ensure functionality, you must set up a dedicated **Python 3.11** kernel.\n",
"\n",
"#### Setup Instructions:\n",
"\n",
"1. Open your **Jupyter Lab** interface.\n",
"2. Open a **Terminal** and execute the following commands:\n",
"```bash\n",
"conda create -n py311 python=3.11\n",
"source ~/.bashrc\n",
"conda activate py311\n",
"conda install jupyter\n",
"python -m ipykernel install --user --name=py311 --display-name \"Python 3.11\"\n",
"pip install py-feat\n",
"pip install \"moviepy<2.0\" # Only if required\n",
"\n",
"```\n",
"\n",
"\n",
"3. **Restart** the kernel.\n",
"4. In **VS Code**, refresh your kernel list and select the one labeled **\"Python 3.11\"**.\n",
"\n",
"---\n",
"\n",
"### Implementation Details\n",
"\n",
"The following code demonstrates a sample integration of the `py-feat` library. The classification output format is determined by the specific AU model selected:\n",
"\n",
"| Model | Output Type | Reference Paper |\n",
"| --- | --- | --- |\n",
"| **SVM** | Binary (0 or 1) | *ADABase* |\n",
"| **XGB** | Floating Point (0.0 - 1.0) | *REVELIO* |\n",
"\n",
"---\n",
"\n",
"Would you like me to provide the Python code block to implement the **SVM** or **XGB** detector using these libraries?"
]
},
{
@@ -1,296 +0,0 @@
import cv2
import time
import os
import threading
from datetime import datetime
from feat import Detector
import torch
import mediapipe as mp
import csv
# Konfiguration
CAMERA_INDEX = 0
OUTPUT_DIR = "recordings"
VIDEO_DURATION = 10 # Sekunden
START_INTERVAL = 5 # Sekunden bis zum nächsten Start
FPS = 25.0 # Feste FPS
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
# Globaler Detector, um ihn nicht bei jedem Video neu laden zu müssen (spart massiv Zeit/Speicher)
print("Initialisiere AU-Detector (bitte warten)...")
detector = Detector(au_model="xgb")
# ===== MediaPipe FaceMesh Setup =====
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(
static_image_mode=False,
max_num_faces=1,
refine_landmarks=True, # wichtig für Iris
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)
LEFT_IRIS = [474, 475, 476, 477]
RIGHT_IRIS = [469, 470, 471, 472]
LEFT_EYE_LIDS = (159, 145)
RIGHT_EYE_LIDS = (386, 374)
LEFT_EYE_GAZE_IDXS = (33, 133, 159, 145)
RIGHT_EYE_GAZE_IDXS = (263, 362, 386, 374)
EYE_OPEN_THRESHOLD = 6
# CSV vorbereiten
gaze_csv = open("gaze_data.csv", mode="w", newline="")
gaze_writer = csv.writer(gaze_csv)
gaze_writer.writerow([
"timestamp",
"left_gaze_x",
"left_gaze_y",
"right_gaze_x",
"right_gaze_y",
"left_valid",
"right_valid",
"left_diameter",
"right_diameter"
])
def eye_openness(landmarks, top_idx, bottom_idx, img_height):
top = landmarks[top_idx]
bottom = landmarks[bottom_idx]
return abs(top.y - bottom.y) * img_height
def compute_gaze(landmarks, iris_center, indices, w, h):
idx1, idx2, top_idx, bottom_idx = indices
p1 = landmarks[idx1]
p2 = landmarks[idx2]
top = landmarks[top_idx]
bottom = landmarks[bottom_idx]
x1 = p1.x * w
x2 = p2.x * w
y_top = top.y * h
y_bottom = bottom.y * h
iris_x, iris_y = iris_center
eye_left = min(x1, x2)
eye_right = max(x1, x2)
eye_width = eye_right - eye_left
eye_height = abs(y_bottom - y_top)
if eye_width == 0 or eye_height == 0:
return 0.5, 0.5
gaze_x = (iris_x - eye_left) / eye_width
gaze_y = (iris_y - min(y_top, y_bottom)) / eye_height
gaze_x = max(0, min(1, gaze_x))
gaze_y = max(0, min(1, gaze_y))
return gaze_x, gaze_y
def extract_aus(path, skip_frames):
# torch.no_grad() deaktiviert die Gradientenberechnung.
# Das löst den "Can't call numpy() on Tensor that requires grad" Fehler.
with torch.no_grad():
video_prediction = detector.detect_video(
path,
skip_frames=skip_frames,
face_detection_threshold=0.95
)
# Falls video_prediction oder .aus noch Tensoren sind,
# stellen wir sicher, dass sie korrekt summiert werden.
try:
# Wir nehmen die Summe der Action Units über alle detektierten Frames
res = video_prediction.aus.sum()
return res
except Exception as e:
print(f"Fehler bei der Summenbildung: {e}")
return 0
def startAU_creation(video_path):
"""Diese Funktion läuft nun in einem eigenen Thread."""
try:
print(f"\n[THREAD START] Analyse läuft für: {video_path}")
# skip_frames berechnen (z.B. alle 5 Sekunden bei 25 FPS = 125)
output = extract_aus(video_path, skip_frames=int(FPS*5))
print(f"\n--- Ergebnis für {os.path.basename(video_path)} ---")
print(output)
print("--------------------------------------------------\n")
except Exception as e:
print(f"Fehler bei der Analyse von {video_path}: {e}")
class VideoRecorder:
def __init__(self, filename, width, height):
self.filename = filename
fourcc = cv2.VideoWriter_fourcc(*'XVID')
self.out = cv2.VideoWriter(filename, fourcc, FPS, (width, height))
self.frames_to_record = int(VIDEO_DURATION * FPS)
self.frames_count = 0
self.is_finished = False
def write_frame(self, frame):
if self.frames_count < self.frames_to_record:
self.out.write(frame)
self.frames_count += 1
else:
self.finish()
def finish(self):
if not self.is_finished:
self.out.release()
self.is_finished = True
abs_path = os.path.abspath(self.filename)
print(f"Video fertig gespeichert: {self.filename}")
# --- MULTITHREADING HIER ---
# Wir starten die Analyse in einem neuen Thread, damit main() sofort weiter frames lesen kann
analysis_thread = threading.Thread(target=startAU_creation, args=(abs_path,))
analysis_thread.daemon = True # Beendet sich, wenn das Hauptprogramm schließt
analysis_thread.start()
def main():
cap = cv2.VideoCapture(CAMERA_INDEX)
if not cap.isOpened():
print("Fehler: Kamera konnte nicht geöffnet werden.")
return
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
active_recorders = []
last_start_time = 0
print("Aufnahme läuft. Drücke 'q' zum Beenden.")
try:
while True:
ret, frame = cap.read()
if not ret:
break
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, _ = frame.shape
results = face_mesh.process(rgb)
left_valid = 0
right_valid = 0
left_diameter = None
right_diameter = None
left_gaze_x = None
left_gaze_y = None
right_gaze_x = None
right_gaze_y = None
if results.multi_face_landmarks:
face_landmarks = results.multi_face_landmarks[0]
left_open = eye_openness(
face_landmarks.landmark,
LEFT_EYE_LIDS[0],
LEFT_EYE_LIDS[1],
h
)
right_open = eye_openness(
face_landmarks.landmark,
RIGHT_EYE_LIDS[0],
RIGHT_EYE_LIDS[1],
h
)
left_valid = 1 if left_open > EYE_OPEN_THRESHOLD else 0
right_valid = 1 if right_open > EYE_OPEN_THRESHOLD else 0
for eye_name, eye_indices in [("left", LEFT_IRIS), ("right", RIGHT_IRIS)]:
iris_points = []
for idx in eye_indices:
lm = face_landmarks.landmark[idx]
x_i, y_i = int(lm.x * w), int(lm.y * h)
iris_points.append((x_i, y_i))
if len(iris_points) == 4:
cx = int(sum(p[0] for p in iris_points) / 4)
cy = int(sum(p[1] for p in iris_points) / 4)
radius = max(
((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
for (x, y) in iris_points
)
diameter = 2 * radius
cv2.circle(frame, (cx, cy), int(radius), (0, 255, 0), 2)
if eye_name == "left" and left_valid:
left_diameter = diameter
left_gaze_x, left_gaze_y = compute_gaze(
face_landmarks.landmark,
(cx, cy),
LEFT_EYE_GAZE_IDXS,
w, h
)
elif eye_name == "right" and right_valid:
right_diameter = diameter
right_gaze_x, right_gaze_y = compute_gaze(
face_landmarks.landmark,
(cx, cy),
RIGHT_EYE_GAZE_IDXS,
w, h
)
# CSV schreiben
gaze_writer.writerow([
time.time(),
left_gaze_x,
left_gaze_y,
right_gaze_x,
right_gaze_y,
left_valid,
right_valid,
left_diameter,
right_diameter
])
current_time = time.time()
if current_time - last_start_time >= START_INTERVAL:
timestamp = datetime.now().strftime("%H%M%S")
filename = os.path.join(OUTPUT_DIR, f"rec_{timestamp}.avi")
new_recorder = VideoRecorder(filename, width, height)
active_recorders.append(new_recorder)
last_start_time = current_time
for rec in active_recorders[:]:
rec.write_frame(frame)
if rec.is_finished:
active_recorders.remove(rec)
cv2.imshow('Kamera Livestream', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
time.sleep(1/FPS)
finally:
gaze_csv.close()
face_mesh.close()
cap.release()
cv2.destroyAllWindows()
print("Programm beendet. Warte ggf. auf laufende Analysen...")
if __name__ == "__main__":
main()
@@ -0,0 +1,174 @@
import cv2
import mediapipe as mp
import numpy as np
import pyautogui
import pandas as pd
import time
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
# Bildschirmgröße
screen_w, screen_h = pyautogui.size()
# MediaPipe Setup
mp_face_mesh = mp.solutions.face_mesh
face_mesh = mp_face_mesh.FaceMesh(refine_landmarks=True)
cap = cv2.VideoCapture(0)
# Iris Landmark Indizes
LEFT_IRIS = [468, 469, 470, 471, 472]
RIGHT_IRIS = [473, 474, 475, 476, 477]
def get_iris_center(landmarks, indices):
points = np.array([[landmarks[i].x, landmarks[i].y] for i in indices])
return np.mean(points, axis=0)
# Kalibrierpunkte
calibration_points = [
(0.1,0.1),(0.5,0.1),(0.9,0.1),
(0.1,0.5),(0.5,0.5),(0.9,0.5),
(0.1,0.9),(0.5,0.9),(0.9,0.9)
]
left_data = []
right_data = []
print("Kalibrierung startet...")
for idx, (px, py) in enumerate(calibration_points):
screen = np.zeros((screen_h, screen_w, 3), dtype=np.uint8)
for j, (cpx, cpy) in enumerate(calibration_points):
cx = int(cpx * screen_w)
cy = int(cpy * screen_h)
if j == idx:
color = (0, 0, 255)
radius = 25
else:
color = (255, 255, 255)
radius = 15
cv2.circle(screen, (cx, cy), radius, color, -1)
# Fenster vorbereiten
cv2.namedWindow("Calibration", cv2.WINDOW_NORMAL)
cv2.imshow("Calibration", screen)
cv2.waitKey(1000)
samples_left = []
samples_right = []
start = time.time()
while time.time() - start < 2:
ret, frame = cap.read()
frame = cv2.flip(frame, 1)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb)
if results.multi_face_landmarks:
mesh = results.multi_face_landmarks[0].landmark
left_center = get_iris_center(mesh, LEFT_IRIS)
right_center = get_iris_center(mesh, RIGHT_IRIS)
samples_left.append(left_center)
samples_right.append(right_center)
avg_left = np.mean(samples_left, axis=0)
avg_right = np.mean(samples_right, axis=0)
target_x = int(px * screen_w)
target_y = int(py * screen_h)
left_data.append([avg_left[0], avg_left[1], target_x, target_y])
right_data.append([avg_right[0], avg_right[1], target_x, target_y])
cv2.destroyWindow("Calibration")
# Training
def train_model(data):
data = np.array(data)
X = data[:, :2]
yx = data[:, 2]
yy = data[:, 3]
model_x = make_pipeline(PolynomialFeatures(2), LinearRegression())
model_y = make_pipeline(PolynomialFeatures(2), LinearRegression())
model_x.fit(X, yx)
model_y.fit(X, yy)
return model_x, model_y
model_lx, model_ly = train_model(left_data)
model_rx, model_ry = train_model(right_data)
print("Kalibrierung abgeschlossen. Tracking startet...")
# Datenaufzeichnung
records = []
while True:
ret, frame = cap.read()
frame = cv2.flip(frame, 1)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = face_mesh.process(rgb)
if results.multi_face_landmarks:
mesh = results.multi_face_landmarks[0].landmark
left_center = get_iris_center(mesh, LEFT_IRIS)
right_center = get_iris_center(mesh, RIGHT_IRIS)
left_input = np.array([left_center])
right_input = np.array([right_center])
lx = model_lx.predict(left_input)[0]
ly = model_ly.predict(left_input)[0]
rx = model_rx.predict(right_input)[0]
ry = model_ry.predict(right_input)[0]
# Pixel-Koordinaten begrenzen
lx = np.clip(lx, 0, screen_w)
ly = np.clip(ly, 0, screen_h)
rx = np.clip(rx, 0, screen_w)
ry = np.clip(ry, 0, screen_h)
# Normierung 0–1
lx_norm = lx / screen_w
ly_norm = ly / screen_h
rx_norm = rx / screen_w
ry_norm = ry / screen_h
records.append([
lx_norm, ly_norm,
rx_norm, ry_norm
])
print("L:", int(lx), int(ly), " | R:", int(rx), int(ry))
cv2.imshow("Tracking", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
print("q gedrückt – beende Tracking")
break
cap.release()
cv2.destroyAllWindows()
# CSV speichern
df = pd.DataFrame(records, columns=[
"EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X",
"EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y",
"EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X",
"EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"
])
df.to_csv("gaze_data1.csv", index=False)
print("Daten gespeichert als gaze_data1.csv")
@@ -1,91 +0,0 @@
import os
import pandas as pd
from pathlib import Path
print(os.getcwd())
num_files = 2 # number of files to process (min: 1, max: 30)
print("connection aufgebaut")
data_dir = Path("/home/jovyan/Fahrsimulator_MSY2526_AI/EDA")
# os.chdir(data_dir)
# Get all .h5 files and sort them
matching_files = sorted(data_dir.glob("*.h5"))
# Chunk size for reading (adjust based on your RAM - 100k rows is ~50-100MB depending on columns)
CHUNK_SIZE = 100_000
for i, file_path in enumerate(matching_files):
print(f"Subject {i} gestartet")
print(f"{file_path} geoeffnet")
# Step 1: Get total number of rows and column names
with pd.HDFStore(file_path, mode="r") as store:
cols = store.select("SIGNALS", start=0, stop=1).columns
nrows = store.get_storer("SIGNALS").nrows
print(f"Total columns: {len(cols)}, Total rows: {nrows}")
# Step 2: Filter columns that start with "FACE_AU"
eye_cols = [c for c in cols if c.startswith("EYE_")]
print(f"eye-tracking columns found: {eye_cols}")
if len(eye_cols) == 0:
print(f"keine eye-tracking-Signale in Subject {i}")
continue
# Columns to read
columns_to_read = ["STUDY", "LEVEL", "PHASE"] + eye_cols
# Step 3: Process file in chunks
chunks_to_save = []
for start_row in range(0, nrows, CHUNK_SIZE):
stop_row = min(start_row + CHUNK_SIZE, nrows)
print(f"Processing rows {start_row} to {stop_row} ({stop_row/nrows*100:.1f}%)")
# Read chunk
df_chunk = pd.read_hdf(
file_path,
key="SIGNALS",
columns=columns_to_read,
start=start_row,
stop=stop_row
)
# Add metadata columns
df_chunk["subjectID"] = i
df_chunk["rowID"] = range(start_row, stop_row)
# Clean data
df_chunk = df_chunk[df_chunk["LEVEL"] != 0]
df_chunk = df_chunk.dropna()
# Only keep non-empty chunks
if len(df_chunk) > 0:
chunks_to_save.append(df_chunk)
# Free memory
del df_chunk
print("load and cleaning done")
# Step 4: Combine all chunks and save
if chunks_to_save:
df_final = pd.concat(chunks_to_save, ignore_index=True)
print(f"Final dataframe shape: {df_final.shape}")
# Save to parquet
base_dir = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_ET_Parquet_files")
os.makedirs(base_dir, exist_ok=True)
out_name = base_dir / f"ET_signals_extracted_{i:04d}.parquet"
df_final.to_parquet(out_name, index=False)
print(f"Saved to {out_name}")
# Free memory
del df_final
del chunks_to_save
else:
print(f"No valid data found for Subject {i}")
print("All files processed!")
@@ -1,91 +0,0 @@
import os
import pandas as pd
from pathlib import Path
print(os.getcwd())
num_files = 2 # number of files to process (min: 1, max: 30)
print("connection aufgebaut")
data_dir = Path(r"C:\Users\x\repo\UXKI\Fahrsimulator_MSY2526_AI\newTmp")
# Get all .h5 files and sort them
matching_files = sorted(data_dir.glob("*.h5"))
# Chunk size for reading (adjust based on your RAM - 100k rows is ~50-100MB depending on columns)
CHUNK_SIZE = 100_000
for i, file_path in enumerate(matching_files):
print(f"Subject {i} gestartet")
print(f"{file_path} geoeffnet")
# Step 1: Get total number of rows and column names
with pd.HDFStore(file_path, mode="r") as store:
cols = store.select("SIGNALS", start=0, stop=1).columns
nrows = store.get_storer("SIGNALS").nrows
print(f"Total columns: {len(cols)}, Total rows: {nrows}")
# Step 2: Filter columns that start with "FACE_AU"
eye_cols = [c for c in cols if c.startswith("FACE_AU")]
print(f"FACE_AU columns found: {eye_cols}")
if len(eye_cols) == 0:
print(f"keine FACE_AU-Signale in Subject {i}")
continue
# Columns to read
columns_to_read = ["STUDY", "LEVEL", "PHASE"] + eye_cols
# Step 3: Process file in chunks
chunks_to_save = []
for start_row in range(0, nrows, CHUNK_SIZE):
stop_row = min(start_row + CHUNK_SIZE, nrows)
print(f"Processing rows {start_row} to {stop_row} ({stop_row/nrows*100:.1f}%)")
# Read chunk
df_chunk = pd.read_hdf(
file_path,
key="SIGNALS",
columns=columns_to_read,
start=start_row,
stop=stop_row
)
# Add metadata columns
df_chunk["subjectID"] = i
df_chunk["rowID"] = range(start_row, stop_row)
# Clean data
df_chunk = df_chunk[df_chunk["LEVEL"] != 0]
df_chunk = df_chunk.dropna()
# Only keep non-empty chunks
if len(df_chunk) > 0:
chunks_to_save.append(df_chunk)
# Free memory
del df_chunk
print("load and cleaning done")
# Step 4: Combine all chunks and save
if chunks_to_save:
df_final = pd.concat(chunks_to_save, ignore_index=True)
print(f"Final dataframe shape: {df_final.shape}")
# Save to parquet
base_dir = Path(r"C:\new_AU_parquet_files")
os.makedirs(base_dir, exist_ok=True)
out_name = base_dir / f"cleaned_{i:04d}.parquet"
df_final.to_parquet(out_name, index=False)
print(f"Saved to {out_name}")
# Free memory
del df_final
del chunks_to_save
else:
print(f"No valid data found for Subject {i}")
print("All files processed!")
+42 -46
View File
@@ -4,27 +4,26 @@ import pandas as pd
from pathlib import Path
from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection
from pygazeanalyser.detectors import fixation_detection, saccade_detection # not installed by default
##############################################################################
# KONFIGURATION
# CONFIGURATION
##############################################################################
INPUT_DIR = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/both_mod_parquet_files")
OUTPUT_FILE = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/50s_25Hz_dataset.parquet")
WINDOW_SIZE_SAMPLES = 25*50 # 50s bei 25Hz
STEP_SIZE_SAMPLES = 125 # 5s bei 25Hz
INPUT_DIR = Path(r"") # directory that stores the parquet files (one file per subject)
OUTPUT_FILE = Path(r"") # path for resulting dataset
WINDOW_SIZE_SAMPLES = 25*50 # 50s at 25Hz
STEP_SIZE_SAMPLES = 125 # 5s at 25Hz
SAMPLING_RATE = 25 # Hz
MIN_DUR_BLINKS = 2 # x * 40ms
##############################################################################
# EYE-TRACKING FUNKTIONEN
# EYE-TRACKING FUNCTIONS
##############################################################################
def clean_eye_df(df):
"""Extrahiert nur Eye-Tracking Spalten und entfernt leere Zeilen."""
"""Extracts Eye-Tracking columns only and removes empty rows."""
eye_cols = [c for c in df.columns if c.startswith("EYE_")]
if not eye_cols:
@@ -38,7 +37,7 @@ def clean_eye_df(df):
def extract_gaze_signal(df):
"""Extrahiert 2D-Gaze-Positionen, maskiert ungültige Samples und interpoliert."""
"""Extracts 2D gaze positions, masks invalid samples, and interpolates."""
gx_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
@@ -51,14 +50,14 @@ def extract_gaze_signal(df):
for arr in [gx_L, gy_L, gx_R, gy_R]:
arr.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige maskieren
# Mask invalids
gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan
# Mittelwert beider Augen
# Mean of both eyes
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
@@ -66,7 +65,7 @@ def extract_gaze_signal(df):
gx = pd.Series(gx).interpolate(limit=None, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=None, limit_direction="both").bfill().ffill()
# MinMax Skalierung
# MinMax scaling
xscaler = MinMaxScaler()
gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
@@ -77,7 +76,7 @@ def extract_gaze_signal(df):
def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt)."""
"""Extract pupil size (average of both eyes)."""
pl = df["EYE_LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["EYE_RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
@@ -96,7 +95,7 @@ def extract_pupil(df):
def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink."""
"""Detect blinks: Validity=0 → Blink."""
blinks = []
start = None
@@ -120,13 +119,13 @@ def compute_IPA(pupil, fs=25):
def extract_eye_features_window(df_eye_window, fs=25, min_dur_blinks=2):
"""
Extrahiert Eye-Tracking Features für ein einzelnes Window.
Gibt Dictionary mit allen Eye-Features zurück.
Extracts eye tracking features for a single window.
Returns a dictionary containing all eye features.
"""
# Gaze
gaze = extract_gaze_signal(df_eye_window)
# Pupille
# Pupil
pupil, pupil_validity = extract_pupil(df_eye_window)
window_size = len(df_eye_window)
@@ -143,7 +142,6 @@ def extract_eye_features_window(df_eye_window, fs=25, min_dur_blinks=2):
fixation_durations = [f[2] for f in efix if np.isfinite(f[2]) and f[2] > 0]
# Kategorien
F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations)
@@ -197,27 +195,27 @@ def extract_eye_features_window(df_eye_window, fs=25, min_dur_blinks=2):
##############################################################################
# KOMBINIERTE FEATURE-EXTRAKTION
# Combined feature extraction
##############################################################################
def process_combined_features(input_dir, output_file, window_size, step_size, fs=25,min_duration_blinks=2):
"""
Verarbeitet Parquet-Dateien mit FACE_AU und EYE Spalten.
Extrahiert beide Feature-Sets und kombiniert sie.
Processes Parquet files with FACE_AU and EYE columns.
Extracts both feature sets and combines them.
"""
input_path = Path(input_dir)
parquet_files = sorted(input_path.glob("*.parquet"))
if not parquet_files:
print(f"FEHLER: Keine Parquet-Dateien in {input_dir} gefunden!")
print(f"Error: No parquet-files found in {input_dir}!")
return None
print(f"\n{'='*70}")
print(f"KOMBINIERTE FEATURE-EXTRAKTION")
print(f"Combined feature-extraction")
print(f"{'='*70}")
print(f"Dateien: {len(parquet_files)}")
print(f"Window: {window_size} Samples ({window_size/fs:.1f}s bei {fs}Hz)")
print(f"Step: {step_size} Samples ({step_size/fs:.1f}s bei {fs}Hz)")
print(f"Files: {len(parquet_files)}")
print(f"Window: {window_size} Samples ({window_size/fs:.1f}s at {fs}Hz)")
print(f"Step: {step_size} Samples ({step_size/fs:.1f}s at {fs}Hz)")
print(f"{'='*70}\n")
all_windows = []
@@ -227,24 +225,22 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
try:
df = pd.read_parquet(parquet_file)
print(f" Einträge: {len(df)}")
print(f" Entries: {len(df)}")
# Identifiziere Spalten
au_columns = [col for col in df.columns if col.startswith('FACE_AU')]
eye_columns = [col for col in df.columns if col.startswith('EYE_')]
print(f" AU-Spalten: {len(au_columns)}")
print(f" Eye-Spalten: {len(eye_columns)}")
print(f" AU-columns: {len(au_columns)}")
print(f" Eye-columns: {len(eye_columns)}")
has_au = len(au_columns) > 0
has_eye = len(eye_columns) > 0
if not has_au and not has_eye:
print(f" WARNUNG: Keine AU oder Eye Spalten gefunden!")
print(f" Warning: No AU or eye tracking columns found!")
continue
# Gruppiere nach STUDY, LEVEL, PHASE
# Group by STUDY, LEVEL, PHASE
group_cols = [col for col in ['STUDY', 'LEVEL', 'PHASE'] if col in df.columns]
if group_cols:
@@ -258,7 +254,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
group_df = group_df.reset_index(drop=True)
# Berechne Anzahl Windows
# calculate number of windows
num_windows = (len(group_df) - window_size) // step_size + 1
if num_windows <= 0:
@@ -272,7 +268,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
window_df = group_df.iloc[start_idx:end_idx]
# Basis-Metadaten
# basic metadata
result = {
'subjectID': window_df['subjectID'].iloc[0],
'start_time': window_df['rowID'].iloc[0],
@@ -281,12 +277,12 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
'PHASE': window_df['PHASE'].iloc[0] if 'PHASE' in window_df.columns else np.nan
}
# FACE AU Features
# FACE AU features
if has_au:
for au_col in au_columns:
result[f'{au_col}_mean'] = window_df[au_col].mean()
# Eye-Tracking Features
# Eye-tracking features
if has_eye:
try:
# clean dataframe from all nan rows
@@ -296,7 +292,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
result.update(eye_features)
except Exception as e:
print(f" WARNUNG: Eye-Features fehlgeschlagen: {str(e)}")
# Füge NaN-Werte für Eye-Features hinzu
# Add NaN-values for eye-features
result.update({
"Fix_count_short_66_150": np.nan,
"Fix_count_medium_300_500": np.nan,
@@ -325,7 +321,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
traceback.print_exc()
continue
# Kombiniere alle Windows
# Combine all windows
if not all_windows:
print("\nKEINE FEATURES EXTRAHIERT!")
return None
@@ -340,7 +336,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
print(f"Spalten: {len(result_df.columns)}")
print(f"Subjects: {result_df['subjectID'].nunique()}")
# Speichern
# Save
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
result_df.to_parquet(output_file, index=False)
@@ -357,7 +353,7 @@ def process_combined_features(input_dir, output_file, window_size, step_size, fs
def main():
print("\n" + "="*70)
print("KOMBINIERTE FEATURE-EXTRAKTION (AU + EYE)")
print("Combined extraction (AU + EYE)")
print("="*70)
result = process_combined_features(
@@ -370,16 +366,16 @@ def main():
)
if result is not None:
print("\nErste 5 Zeilen:")
print("\First 5 rows:")
print(result.head())
print("\nSpalten-Übersicht:")
print("\nColumns overview:")
print(result.dtypes)
print("\nStatistik:")
print("\Statistics:")
print(result.describe())
print("\n✓ FERTIG!\n")
print("\nDone!\n")
if __name__ == "__main__":
-113
View File
@@ -1,113 +0,0 @@
import pandas as pd
import numpy as np
from pathlib import Path
def process_parquet_files(input_dir, output_file, window_size=1250, step_size=125):
"""
Verarbeitet Parquet-Dateien mit Sliding Window Aggregation.
Parameters:
-----------
input_dir : str
Verzeichnis mit Parquet-Dateien
output_file : str
Pfad für die Ausgabe-Parquet-Datei
window_size : int
Größe des Sliding Windows (default: 3000)
step_size : int
Schrittweite in Einträgen (default: 250 = 10 Sekunden bei 25 Hz)
"""
input_path = Path(input_dir)
parquet_files = sorted(input_path.glob("*.parquet"))
if not parquet_files:
print(f"Keine Parquet-Dateien in {input_dir} gefunden!")
return
print(f"Gefundene Dateien: {len(parquet_files)}")
all_windows = []
for file_idx, parquet_file in enumerate(parquet_files):
print(f"\nVerarbeite Datei {file_idx + 1}/{len(parquet_files)}: {parquet_file.name}")
# Lade Parquet-Datei
df = pd.read_parquet(parquet_file)
print(f" Einträge: {len(df)}")
# Identifiziere AU-Spalten
au_columns = [col for col in df.columns if col.startswith('FACE_AU')]
print(f" AU-Spalten: {len(au_columns)}")
# Gruppiere nach STUDY, LEVEL, PHASE (um Übergänge zu vermeiden)
for (study_val, level_val, phase_val), level_df in df.groupby(['STUDY', 'LEVEL', 'PHASE'], sort=False):
print(f" STUDY {study_val}, LEVEL {level_val}, PHASE {phase_val}: {len(level_df)} Einträge")
# Reset index für korrekte Position-Berechnung
level_df = level_df.reset_index(drop=True)
# Sliding Window über dieses Level
num_windows = (len(level_df) - window_size) // step_size + 1
if num_windows <= 0:
print(f" Zu wenige Einträge für Window (benötigt {window_size})")
continue
for i in range(num_windows):
start_idx = i * step_size
end_idx = start_idx + window_size
window_df = level_df.iloc[start_idx:end_idx]
# Erstelle aggregiertes Ergebnis
result = {
'subjectID': window_df['subjectID'].iloc[0],
'start_time': window_df['rowID'].iloc[0], # rowID als start_time
'STUDY': window_df['STUDY'].iloc[0],
'LEVEL': window_df['LEVEL'].iloc[0],
'PHASE': window_df['PHASE'].iloc[0]
}
# Summiere alle AU-Spalten
for au_col in au_columns:
# result[f'{au_col}_sum'] = window_df[au_col].sum()
result[f'{au_col}_mean'] = window_df[au_col].mean()
all_windows.append(result)
print(f" Windows erstellt: {num_windows}")
# Erstelle finalen DataFrame
result_df = pd.DataFrame(all_windows)
print(f"\n{'='*60}")
print(f"Gesamt Windows erstellt: {len(result_df)}")
print(f"Spalten: {list(result_df.columns)}")
# Speichere Ergebnis
result_df.to_parquet(output_file, index=False)
print(f"\nErgebnis gespeichert in: {output_file}")
return result_df
# Beispiel-Verwendung
if __name__ == "__main__":
# Anpassen an deine Pfade
input_directory = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_AU_parquet_files")
output_file = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_AU_dataset_mean/AU_dataset_mean.parquet")
result = process_parquet_files(
input_dir=input_directory,
output_file=output_file,
window_size=1250,
step_size=125
)
# Zeige erste Zeilen
if result is not None:
print("\nErste 5 Zeilen des Ergebnisses:")
print(result.head())
@@ -1,56 +0,0 @@
from pathlib import Path
import pandas as pd
def main():
"""
USER CONFIGURATION
------------------
Specify input files and output directory here.
"""
# Input parquet files (single-modality datasets)
file_modality_1 = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/AU_dataset_mean.parquet")
file_modality_2 = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/new_eye_dataset.parquet")
# Output directory and file name
output_dir = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/")
output_file = output_dir / "merged_dataset.parquet"
# Column names (adjust only if your schema differs)
subject_col = "subjectID"
time_col = "start_time"
# ------------------------------------------------------------------
# Load datasets
# ------------------------------------------------------------------
df1 = pd.read_parquet(file_modality_1)
df2 = pd.read_parquet(file_modality_2)
# ------------------------------------------------------------------
# Keep only subjects that appear in BOTH datasets
# ------------------------------------------------------------------
common_subjects = set(df1[subject_col]).intersection(df2[subject_col])
df1 = df1[df1[subject_col].isin(common_subjects)]
df2 = df2[df2[subject_col].isin(common_subjects)]
# ------------------------------------------------------------------
# Inner join on subject ID AND start_time
# ------------------------------------------------------------------
merged_df = pd.merge(
df1,
df2,
on=[subject_col, time_col],
how="inner",
)
# ------------------------------------------------------------------
# Save merged dataset
# ------------------------------------------------------------------
output_dir.mkdir(parents=True, exist_ok=True)
merged_df.to_parquet(output_file, index=False)
if __name__ == "__main__":
main()
@@ -1,6 +1,5 @@
# pip install pyocclient
import yaml
import owncloud
import owncloud # pip install pyocclient
import pandas as pd
import h5py
import os
@@ -26,7 +25,7 @@ for i in range(num_files):
# Download file from ownCloud
oc.get_file(file_name, local_tmp)
print(f"{file_name} geoeffnet")
print(f"Opened: {file_name}")
# Load into memory and extract needed columns
# with h5py.File(local_tmp, "r") as f:
# # Adjust this path depending on actual dataset layout inside .h5py file
@@ -35,14 +34,9 @@ for i in range(num_files):
with pd.HDFStore(local_tmp, mode="r") as store:
cols = store.select("SIGNALS", start=0, stop=1).columns # get column names
# Step 2: Filter columns that start with "AU"
au_cols = [c for c in cols if c.startswith("AU")]
print(au_cols)
if len(au_cols)==0:
print(f"keine AU Signale in Subject {i}")
continue
# Step 3: Read only those columns (plus any others you want)
df = pd.read_hdf(local_tmp, key="SIGNALS", columns=["STUDY", "LEVEL", "PHASE"] + au_cols)
df = pd.read_hdf(local_tmp, key="SIGNALS", columns=["STUDY", "LEVEL", "PHASE"] + cols)
print("load done")
@@ -63,7 +57,7 @@ for i in range(num_files):
# Save to parquet
os.makedirs("ParquetFiles", exist_ok=True)
os.makedirs("ParquetFiles", exist_ok=True) # TODO: change for custom directory
out_name = f"ParquetFiles/cleaned_{i:04d}.parquet"
df.to_parquet(out_name, index=False)
-323
View File
@@ -1,323 +0,0 @@
import numpy as np
import pandas as pd
import h5py
import yaml
import os
from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection
##############################################################################
# 1. HELFERFUNKTIONEN
##############################################################################
def clean_eye_df(df):
"""
Entfernt alle Zeilen, die keine echten Eyetracking-Daten enthalten.
Löst das Problem, dass das Haupt-DataFrame NaN-Zeilen für andere Sensoren enthält.
"""
eye_cols = [c for c in df.columns if ("LEFT_" in c or "RIGHT_" in c)]
df_eye = df[eye_cols]
# INF → NaN
df_eye = df_eye.replace([np.inf, -np.inf], np.nan)
# Nur Zeilen behalten, wo es echte Eyetracking-Daten gibt
df_eye = df_eye.dropna(subset=eye_cols, how="all")
print("Eyetracking-Zeilen vorher:", len(df))
print("Eyetracking-Zeilen nachher:", len(df_eye))
#Index zurücksetzen
return df_eye.reset_index(drop=True)
def extract_gaze_signal(df):
"""
Extrahiert 2D-Gaze-Positionen auf dem Display,
maskiert ungültige Samples und interpoliert Lücken.
"""
print("→ extract_gaze_signal(): Eingabegröße:", df.shape)
# Gaze-Spalten
gx_L = df["LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_R = df["RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
# Validity-Spalten (1 = gültig)
val_L = (df["LEFT_GAZE_POINT_VALIDITY"] == 1)
val_R = (df["RIGHT_GAZE_POINT_VALIDITY"] == 1)
# Inf ersetzen mit NaN (kommt bei Tobii bei Blinks vor)
gx_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gx_R.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_R.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige Werte maskieren
gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan
# Mittelwert der beiden Augen pro Sample (nanmean ist robust)
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
# Interpolation (wichtig für PyGaze!)
gx = pd.Series(gx).interpolate(limit=50, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=50, limit_direction="both").bfill().ffill()
# xscaler = MinMaxScaler()
# gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
# yscaler = MinMaxScaler()
# gyscale = yscaler.fit_transform(gx.values.reshape(-1, 1))
#print("xmax ymax", gxscale.max(), gyscale.max())
#out = np.column_stack((gxscale, gyscale))
out = np.column_stack((gx, gy))
print("→ extract_gaze_signal(): Ausgabegröße:", out.shape)
return out
def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt)."""
pl = df["LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
vl = df.get("LEFT_PUPIL_VALIDITY")
vr = df.get("RIGHT_PUPIL_VALIDITY")
if vl is None or vr is None:
# Falls Validity-Spalten nicht vorhanden sind, versuchen wir grobe Heuristik:
# gültig, wenn Pupillendurchmesser nicht NaN.
validity = (~pl.isna() | ~pr.isna()).astype(int).to_numpy()
else:
# Falls vorhanden: 1 wenn mindestens eines der Augen gültig ist
validity = ( (vl == 1) | (vr == 1) ).astype(int).to_numpy()
# Mittelwert der verfügbaren Pupillen
p = np.mean(np.column_stack([pl, pr]), axis=1)
# INF/NaN reparieren
p = pd.Series(p).interpolate(limit=50, limit_direction="both").bfill().ffill()
p = p.to_numpy()
print("→ extract_pupil(): Pupillensignal Länge:", len(p))
return p, validity
def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink."""
blinks = []
start = None
for i, v in enumerate(pupil_validity):
if v == 0 and start is None:
start = i
elif v == 1 and start is not None:
if i - start >= min_duration:
blinks.append([start, i])
start = None
return blinks
def compute_IPA(pupil, fs=250):
"""
IPA = Index of Pupillary Activity (nach Duchowski 2018).
Hochfrequenzanteile der Pupillenzeitreihe.
"""
f, Pxx = welch(pupil, fs=fs, nperseg=int(fs*2)) # 2 Sekunden Fenster
hf_band = (f >= 0.6) & (f <= 2.0)
ipa = np.sum(Pxx[hf_band])
return ipa
##############################################################################
# 2. FEATURE-EXTRAKTION (HAUPTFUNKTION)
##############################################################################
def extract_eye_features(df, window_length_sec=50, fs=250):
"""
df = Tobii DataFrame
window_length_sec = Fenstergröße (z.B. W=1s)
"""
print("→ extract_eye_features(): Starte Feature-Berechnung...")
print(" Fensterlänge W =", window_length_sec, "s")
W = int(window_length_sec * fs) # Window größe in Samples
# Gaze
gaze = extract_gaze_signal(df)
gx, gy = gaze[:, 0], gaze[:, 1]
print("Gültige Werte (gx):", np.sum(~np.isnan(gx)), "von", len(gx))
print("Range:", np.nanmin(gx), np.nanmax(gx))
print("Gültige Werte (gy):", np.sum(~np.isnan(gy)), "von", len(gy))
print("Range:", np.nanmin(gy), np.nanmax(gy))
# Pupille
pupil, pupil_validity = extract_pupil(df)
features = []
# Sliding windows
for start in range(0, len(df), W):
end = start + W
if end > len(df):
break #das letzte Fenster wird ignoriert
w_gaze = gaze[start:end]
w_pupil = pupil[start:end]
w_valid = pupil_validity[start:end]
# ----------------------------
# FIXATIONS (PyGaze)
# ----------------------------
time_ms = np.arange(W) * 1000.0 / fs
# print("gx im Fenster:", w_gaze[:,0][:20])
# print("gy im Fenster:", w_gaze[:,1][:20])
# print("gx diff:", np.mean(np.abs(np.diff(w_gaze[:,0]))))
# print("Werte X im Fenster:", w_gaze[:,0])
# print("Werte Y im Fenster:", w_gaze[:,1])
# print("X-Stats: min/max/diff", np.nanmin(w_gaze[:,0]), np.nanmax(w_gaze[:,0]), np.nanmean(np.abs(np.diff(w_gaze[:,0]))))
# print("Y-Stats: min/max/diff", np.nanmin(w_gaze[:,1]), np.nanmax(w_gaze[:,1]), np.nanmean(np.abs(np.diff(w_gaze[:,1]))))
print("time_ms:", time_ms)
fix, efix = fixation_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0.0, maxdist=0.003, mindur=10 # mindur=100ms
)
#print("Raw Fixation Output:", efix[0])
if start == 0:
print("DEBUG fix raw:", fix[:10])
# Robust fixations: PyGaze may return malformed entries
fixation_durations = []
for f in efix:
print("Efix:", f[2])
# start_t = f[1] # in ms
# end_t = f[2] # in ms
# duration = (end_t - start_t) / 1000.0 # in Sekunden
#duration = f[2] / 1000.0
if np.isfinite(f[2]) and f[2] > 0:
fixation_durations.append(f[2])
# Kategorien laut Paper
F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations)
F_hundred = sum(d > 100 for d in fixation_durations)
F_Cancel = sum(66 < d for d in fixation_durations)
# ----------------------------
# SACCADES
# ----------------------------
sac, esac = saccade_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms, missing=0, minlen=12, maxvel=0.2, maxacc=1
)
sac_durations = [s[2] for s in esac]
sac_amplitudes = [((s[5]-s[3])**2 + (s[6]-s[4])**2)**0.5 for s in esac]
# ----------------------------
# BLINKS
# ----------------------------
blinks = detect_blinks(w_valid)
blink_durations = [(b[1] - b[0]) / fs for b in blinks]
# ----------------------------
# PUPIL
# ----------------------------
if np.all(np.isnan(w_pupil)):
mean_pupil = np.nan
ipa = np.nan
else:
mean_pupil = np.nanmean(w_pupil)
ipa = compute_IPA(w_pupil, fs=fs)
# ----------------------------
# FEATURE-TABELLE FÜLLEN
# ----------------------------
features.append({
"Fix_count_short_66_150": F_short,
"Fix_count_medium_300_500": F_medium,
"Fix_count_long_gt_1000": F_long,
"Fix_count_100": F_hundred,
"Fix_cancel": F_Cancel,
"Fix_mean_duration": np.mean(fixation_durations) if fixation_durations else 0,
"Fix_median_duration": np.median(fixation_durations) if fixation_durations else 0,
"Sac_count": len(sac),
"Sac_mean_amp": np.mean(sac_amplitudes) if sac_amplitudes else 0,
"Sac_mean_dur": np.mean(sac_durations) if sac_durations else 0,
"Sac_median_dur": np.median(sac_durations) if sac_durations else 0,
"Blink_count": len(blinks),
"Blink_mean_dur": np.mean(blink_durations) if blink_durations else 0,
"Blink_median_dur": np.median(blink_durations) if blink_durations else 0,
"Pupil_mean": mean_pupil,
"Pupil_IPA": ipa
})
result = pd.DataFrame(features)
print("→ extract_eye_features(): Fertig! Ergebnisgröße:", result.shape)
return result
##############################################################################
# 3. MAIN FUNKTION
##############################################################################
def main():
print("### STARTE FEATURE-EXTRAKTION ###")
print("Aktueller Arbeitsordner:", os.getcwd())
#df = pd.read_hdf("tmp22.h5", "SIGNALS", mode="r")
df = pd.read_parquet("cleaned_0001.parquet")
print("DataFrame geladen:", df.shape)
# Nur Eye-Tracking auswählen
#eye_cols = [c for c in df.columns if "EYE_" in c]
#df_eye = df[eye_cols]
#print("Eye-Tracking-Spalten:", len(eye_cols))
#print("→", eye_cols[:10], " ...")
print("Reinige Eyetracking-Daten ...")
df_eye = clean_eye_df(df)
# Feature Extraction
features = extract_eye_features(df_eye, window_length_sec=50, fs=250)
print("\n### FEATURE-MATRIX (HEAD) ###")
print(features.head())
print("\nSpeichere Output in features.csv ...")
features.to_csv("features4.csv", index=False)
print("FERTIG!")
if __name__ == "__main__":
main()
-441
View File
@@ -1,441 +0,0 @@
import numpy as np
import pandas as pd
import h5py
import yaml
import os
from pathlib import Path
from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection
##############################################################################
# KONFIGURATION - HIER ANPASSEN!
##############################################################################
INPUT_DIR = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/new_ET_Parquet_files/")
OUTPUT_FILE = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/Eye_dataset_old/new_eye_dataset.parquet")
WINDOW_SIZE_SAMPLES = 12500 # Anzahl Samples pro Window (z.B. 1250 = 50s bei 25Hz, oder 5s bei 250Hz)
STEP_SIZE_SAMPLES = 1250 # Schrittweite (z.B. 125 = 5s bei 25Hz, oder 0.5s bei 250Hz)
SAMPLING_RATE = 250 # Hz
##############################################################################
# 1. HELFERFUNKTIONEN
##############################################################################
def clean_eye_df(df):
"""
Entfernt alle Zeilen, die keine echten Eyetracking-Daten enthalten.
Löst das Problem, dass das Haupt-DataFrame NaN-Zeilen für andere Sensoren enthält.
"""
eye_cols = [c for c in df.columns if c.startswith("EYE_")]
df_eye = df[eye_cols]
# INF → NaN
df_eye = df_eye.replace([np.inf, -np.inf], np.nan)
# Nur Zeilen behalten, wo es echte Eyetracking-Daten gibt
df_eye = df_eye.dropna(subset=eye_cols, how="all")
print(f" Eyetracking-Zeilen: {len(df)} → {len(df_eye)}")
return df_eye.reset_index(drop=True)
def extract_gaze_signal(df):
"""
Extrahiert 2D-Gaze-Positionen auf dem Display,
maskiert ungültige Samples und interpoliert Lücken.
"""
# Gaze-Spalten
gx_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
# Validity-Spalten (1 = gültig)
val_L = (df["EYE_LEFT_GAZE_POINT_VALIDITY"] == 1)
val_R = (df["EYE_RIGHT_GAZE_POINT_VALIDITY"] == 1)
# Inf ersetzen mit NaN (kommt bei Tobii bei Blinks vor)
gx_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gx_R.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_R.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige Werte maskieren
gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan
# Mittelwert der beiden Augen pro Sample (nanmean ist robust)
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
# Interpolation (wichtig für PyGaze!)
gx = pd.Series(gx).interpolate(limit=50, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=50, limit_direction="both").bfill().ffill()
xscaler = MinMaxScaler()
gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
yscaler = MinMaxScaler()
gyscale = yscaler.fit_transform(gy.values.reshape(-1, 1))
out = np.column_stack((gxscale, gyscale))
return out
def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt)."""
pl = df["EYE_LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["EYE_RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
vl = df.get("EYE_LEFT_PUPIL_VALIDITY")
vr = df.get("EYE_RIGHT_PUPIL_VALIDITY")
if vl is None or vr is None:
validity = (~pl.isna() | ~pr.isna()).astype(int).to_numpy()
else:
validity = ((vl == 1) | (vr == 1)).astype(int).to_numpy()
# Mittelwert der verfügbaren Pupillen
p = np.mean(np.column_stack([pl, pr]), axis=1)
# INF/NaN reparieren
p = pd.Series(p).interpolate(limit=50, limit_direction="both").bfill().ffill()
p = p.to_numpy()
return p, validity
def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink."""
blinks = []
start = None
for i, v in enumerate(pupil_validity):
if v == 0 and start is None:
start = i
elif v == 1 and start is not None:
if i - start >= min_duration:
blinks.append([start, i])
start = None
return blinks
def compute_IPA(pupil, fs=250):
"""
IPA = Index of Pupillary Activity (nach Duchowski 2018).
Hochfrequenzanteile der Pupillenzeitreihe.
"""
f, Pxx = welch(pupil, fs=fs, nperseg=int(fs*2)) # 2 Sekunden Fenster
hf_band = (f >= 0.6) & (f <= 2.0)
ipa = np.sum(Pxx[hf_band])
return ipa
##############################################################################
# 2. FEATURE-EXTRAKTION MIT SLIDING WINDOW
##############################################################################
def extract_eye_features_sliding(df_eye, df_meta, window_size, step_size, fs=250):
"""
Extrahiert Features mit Sliding Window aus einem einzelnen Level/Phase.
Parameters:
-----------
df_eye : DataFrame
Eye-Tracking Daten (bereits gereinigt)
df_meta : DataFrame
Metadaten (subjectID, rowID, STUDY, LEVEL, PHASE)
window_size : int
Anzahl Samples pro Window
step_size : int
Schrittweite in Samples
fs : int
Sampling Rate in Hz
"""
# Gaze
gaze = extract_gaze_signal(df_eye)
# Pupille
pupil, pupil_validity = extract_pupil(df_eye)
features = []
num_windows = (len(df_eye) - window_size) // step_size + 1
if num_windows <= 0:
return pd.DataFrame()
for i in range(num_windows):
start_idx = i * step_size
end_idx = start_idx + window_size
w_gaze = gaze[start_idx:end_idx]
w_pupil = pupil[start_idx:end_idx]
w_valid = pupil_validity[start_idx:end_idx]
# Metadaten für dieses Window
meta_row = df_meta.iloc[start_idx]
# ----------------------------
# FIXATIONS (PyGaze)
# ----------------------------
time_ms = np.arange(window_size) * 1000.0 / fs
fix, efix = fixation_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0.0, maxdist=0.003, mindur=10
)
fixation_durations = []
for f in efix:
if np.isfinite(f[2]) and f[2] > 0:
fixation_durations.append(f[2])
# Kategorien laut Paper
F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations)
F_hundred = sum(d > 100 for d in fixation_durations)
# F_Cancel = sum(66 < d for d in fixation_durations)
# ----------------------------
# SACCADES
# ----------------------------
sac, esac = saccade_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0, minlen=12, maxvel=0.2, maxacc=1
)
sac_durations = [s[2] for s in esac]
sac_amplitudes = [((s[5]-s[3])**2 + (s[6]-s[4])**2)**0.5 for s in esac]
# ----------------------------
# BLINKS
# ----------------------------
blinks = detect_blinks(w_valid)
blink_durations = [(b[1] - b[0]) / fs for b in blinks]
# ----------------------------
# PUPIL
# ----------------------------
if np.all(np.isnan(w_pupil)):
mean_pupil = np.nan
ipa = np.nan
else:
mean_pupil = np.nanmean(w_pupil)
ipa = compute_IPA(w_pupil, fs=fs)
# ----------------------------
# FEATURE-DICTIONARY
# ----------------------------
features.append({
# Metadaten
'subjectID': meta_row['subjectID'],
'start_time': meta_row['rowID'],
'STUDY': meta_row.get('STUDY', np.nan),
'LEVEL': meta_row.get('LEVEL', np.nan),
'PHASE': meta_row.get('PHASE', np.nan),
# Fixation Features
"Fix_count_short_66_150": F_short,
"Fix_count_medium_300_500": F_medium,
"Fix_count_long_gt_1000": F_long,
"Fix_count_100": F_hundred,
# "Fix_cancel": F_Cancel,
"Fix_mean_duration": np.mean(fixation_durations) if fixation_durations else 0,
"Fix_median_duration": np.median(fixation_durations) if fixation_durations else 0,
# Saccade Features
"Sac_count": len(sac),
"Sac_mean_amp": np.mean(sac_amplitudes) if sac_amplitudes else 0,
"Sac_mean_dur": np.mean(sac_durations) if sac_durations else 0,
"Sac_median_dur": np.median(sac_durations) if sac_durations else 0,
# Blink Features
"Blink_count": len(blinks),
"Blink_mean_dur": np.mean(blink_durations) if blink_durations else 0,
"Blink_median_dur": np.median(blink_durations) if blink_durations else 0,
# Pupil Features
"Pupil_mean": mean_pupil,
"Pupil_IPA": ipa
})
return pd.DataFrame(features)
##############################################################################
# 3. BATCH-VERARBEITUNG
##############################################################################
def process_parquet_directory(input_dir, output_file, window_size, step_size, fs=250):
"""
Verarbeitet alle Parquet-Dateien in einem Verzeichnis.
Parameters:
-----------
input_dir : str
Pfad zum Verzeichnis mit Parquet-Dateien
output_file : str
Pfad für die Ausgabe-Parquet-Datei
window_size : int
Window-Größe in Samples
step_size : int
Schrittweite in Samples
fs : int
Sampling Rate in Hz
"""
input_path = Path(input_dir)
parquet_files = sorted(input_path.glob("*.parquet"))
if not parquet_files:
print(f"FEHLER: Keine Parquet-Dateien in {input_dir} gefunden!")
return
print(f"\n{'='*70}")
print(f"STARTE BATCH-VERARBEITUNG")
print(f"{'='*70}")
print(f"Gefundene Dateien: {len(parquet_files)}")
print(f"Window Size: {window_size} Samples ({window_size/fs:.1f}s bei {fs}Hz)")
print(f"Step Size: {step_size} Samples ({step_size/fs:.1f}s bei {fs}Hz)")
print(f"{'='*70}\n")
all_features = []
for file_idx, parquet_file in enumerate(parquet_files, 1):
print(f"\n[{file_idx}/{len(parquet_files)}] Verarbeite: {parquet_file.name}")
try:
# Lade Parquet-Datei
df = pd.read_parquet(parquet_file)
print(f" Einträge geladen: {len(df)}")
# Prüfe ob benötigte Spalten vorhanden sind
required_cols = ['subjectID', 'rowID']
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
print(f" WARNUNG: Fehlende Spalten: {missing_cols} - Überspringe Datei")
continue
# Reinige Eye-Tracking-Daten
df_eye = clean_eye_df(df)
if len(df_eye) == 0:
print(f" WARNUNG: Keine gültigen Eye-Tracking-Daten - Überspringe Datei")
continue
# Metadaten extrahieren (aligned mit df_eye)
meta_cols = ['subjectID', 'rowID']
if 'STUDY' in df.columns:
meta_cols.append('STUDY')
if 'LEVEL' in df.columns:
meta_cols.append('LEVEL')
if 'PHASE' in df.columns:
meta_cols.append('PHASE')
df_meta = df[meta_cols].iloc[df_eye.index].reset_index(drop=True)
# Gruppiere nach STUDY, LEVEL, PHASE (falls vorhanden)
group_cols = [col for col in ['STUDY', 'LEVEL', 'PHASE'] if col in df_meta.columns]
if group_cols:
print(f" Gruppiere nach: {', '.join(group_cols)}")
for group_vals, group_df in df_meta.groupby(group_cols, sort=False):
group_eye = df_eye.iloc[group_df.index].reset_index(drop=True)
group_meta = group_df.reset_index(drop=True)
print(f" Gruppe {group_vals}: {len(group_eye)} Samples", end=" → ")
features_df = extract_eye_features_sliding(
group_eye, group_meta, window_size, step_size, fs
)
if not features_df.empty:
all_features.append(features_df)
print(f"{len(features_df)} Windows")
else:
print("Zu wenige Daten")
else:
# Keine Gruppierung
print(f" Keine Gruppierungsspalten gefunden")
features_df = extract_eye_features_sliding(
df_eye, df_meta, window_size, step_size, fs
)
if not features_df.empty:
all_features.append(features_df)
print(f" → {len(features_df)} Windows erstellt")
else:
print(f" → Zu wenige Daten")
except Exception as e:
print(f" FEHLER bei Verarbeitung: {str(e)}")
import traceback
traceback.print_exc()
continue
# Kombiniere alle Features
if not all_features:
print("\nKEINE FEATURES EXTRAHIERT!")
return None
print(f"\n{'='*70}")
print(f"ZUSAMMENFASSUNG")
print(f"{'='*70}")
final_df = pd.concat(all_features, ignore_index=True)
print(f"Gesamt Windows: {len(final_df)}")
print(f"Spalten: {len(final_df.columns)}")
print(f"Subjects: {final_df['subjectID'].nunique()}")
# Speichere Ergebnis
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
final_df.to_parquet(output_file, index=False)
print(f"\n✓ Ergebnis gespeichert: {output_file}")
print(f"{'='*70}\n")
return final_df
##############################################################################
# 4. MAIN
##############################################################################
def main():
print("\n" + "="*70)
print("EYE-TRACKING FEATURE EXTRAKTION - BATCH MODE")
print("="*70)
result = process_parquet_directory(
input_dir=INPUT_DIR,
output_file=OUTPUT_FILE,
window_size=WINDOW_SIZE_SAMPLES,
step_size=STEP_SIZE_SAMPLES,
fs=SAMPLING_RATE
)
if result is not None:
print("\nErste 5 Zeilen des Ergebnisses:")
print(result.head())
print("\nSpalten-Übersicht:")
print(result.columns.tolist())
print("\nDatentypen:")
print(result.dtypes)
print("\n✓ FERTIG!\n")
if __name__ == "__main__":
main()
-323
View File
@@ -1,323 +0,0 @@
import numpy as np
import pandas as pd
import h5py
import yaml
import owncloud
import os
from sklearn.preprocessing import MinMaxScaler
from scipy.signal import welch
from pygazeanalyser.detectors import fixation_detection, saccade_detection
##############################################################################
# 1. HELFERFUNKTIONEN
##############################################################################
def clean_eye_df(df):
"""
Entfernt alle Zeilen, die keine echten Eyetracking-Daten enthalten.
Löst das Problem, dass das Haupt-DataFrame NaN-Zeilen für andere Sensoren enthält.
"""
eye_cols = [c for c in df.columns if "EYE_" in c]
df_eye = df[eye_cols]
# INF → NaN
df_eye = df_eye.replace([np.inf, -np.inf], np.nan)
# Nur Zeilen behalten, wo es echte Eyetracking-Daten gibt
df_eye = df_eye.dropna(subset=eye_cols, how="all")
print("Eyetracking-Zeilen vorher:", len(df))
print("Eyetracking-Zeilen nachher:", len(df_eye))
#Index zurücksetzen
return df_eye.reset_index(drop=True)
def extract_gaze_signal(df):
"""
Extrahiert 2D-Gaze-Positionen auf dem Display,
maskiert ungültige Samples und interpoliert Lücken.
"""
print("→ extract_gaze_signal(): Eingabegröße:", df.shape)
# Gaze-Spalten
gx_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_L = df["EYE_LEFT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
gx_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_X"].astype(float).copy()
gy_R = df["EYE_RIGHT_GAZE_POINT_ON_DISPLAY_AREA_Y"].astype(float).copy()
# Validity-Spalten (1 = gültig)
val_L = (df["EYE_LEFT_GAZE_POINT_VALIDITY"] == 1)
val_R = (df["EYE_RIGHT_GAZE_POINT_VALIDITY"] == 1)
# Inf ersetzen mit NaN (kommt bei Tobii bei Blinks vor)
gx_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_L.replace([np.inf, -np.inf], np.nan, inplace=True)
gx_R.replace([np.inf, -np.inf], np.nan, inplace=True)
gy_R.replace([np.inf, -np.inf], np.nan, inplace=True)
# Ungültige Werte maskieren
gx_L[~val_L] = np.nan
gy_L[~val_L] = np.nan
gx_R[~val_R] = np.nan
gy_R[~val_R] = np.nan
# Mittelwert der beiden Augen pro Sample (nanmean ist robust)
gx = np.mean(np.column_stack([gx_L, gx_R]), axis=1)
gy = np.mean(np.column_stack([gy_L, gy_R]), axis=1)
# Interpolation (wichtig für PyGaze!)
gx = pd.Series(gx).interpolate(limit=50, limit_direction="both").bfill().ffill()
gy = pd.Series(gy).interpolate(limit=50, limit_direction="both").bfill().ffill()
xscaler = MinMaxScaler()
gxscale = xscaler.fit_transform(gx.values.reshape(-1, 1))
yscaler = MinMaxScaler()
gyscale = yscaler.fit_transform(gx.values.reshape(-1, 1))
print("xmax ymax", gxscale.max(), gyscale.max())
out = np.column_stack((gxscale, gyscale))
print("→ extract_gaze_signal(): Ausgabegröße:", out.shape)
return out
def extract_pupil(df):
"""Extrahiert Pupillengröße (beide Augen gemittelt)."""
pl = df["EYE_LEFT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
pr = df["EYE_RIGHT_PUPIL_DIAMETER"].replace([np.inf, -np.inf], np.nan)
vl = df.get("EYE_LEFT_PUPIL_VALIDITY")
vr = df.get("EYE_RIGHT_PUPIL_VALIDITY")
if vl is None or vr is None:
# Falls Validity-Spalten nicht vorhanden sind, versuchen wir grobe Heuristik:
# gültig, wenn Pupillendurchmesser nicht NaN.
validity = (~pl.isna() | ~pr.isna()).astype(int).to_numpy()
else:
# Falls vorhanden: 1 wenn mindestens eines der Augen gültig ist
validity = ( (vl == 1) | (vr == 1) ).astype(int).to_numpy()
# Mittelwert der verfügbaren Pupillen
p = np.mean(np.column_stack([pl, pr]), axis=1)
# INF/NaN reparieren
p = pd.Series(p).interpolate(limit=50, limit_direction="both").bfill().ffill()
p = p.to_numpy()
print("→ extract_pupil(): Pupillensignal Länge:", len(p))
return p, validity
def detect_blinks(pupil_validity, min_duration=5):
"""Erkennt Blinks: Validity=0 → Blink."""
blinks = []
start = None
for i, v in enumerate(pupil_validity):
if v == 0 and start is None:
start = i
elif v == 1 and start is not None:
if i - start >= min_duration:
blinks.append([start, i])
start = None
return blinks
def compute_IPA(pupil, fs=250):
"""
IPA = Index of Pupillary Activity (nach Duchowski 2018).
Hochfrequenzanteile der Pupillenzeitreihe.
"""
f, Pxx = welch(pupil, fs=fs, nperseg=int(fs*2)) # 2 Sekunden Fenster
hf_band = (f >= 0.6) & (f <= 2.0)
ipa = np.sum(Pxx[hf_band])
return ipa
##############################################################################
# 2. FEATURE-EXTRAKTION (HAUPTFUNKTION)
##############################################################################
def extract_eye_features(df, window_length_sec=50, fs=250):
"""
df = Tobii DataFrame
window_length_sec = Fenstergröße (z.B. W=1s)
"""
print("→ extract_eye_features(): Starte Feature-Berechnung...")
print(" Fensterlänge W =", window_length_sec, "s")
W = int(window_length_sec * fs) # Window größe in Samples
# Gaze
gaze = extract_gaze_signal(df)
gx, gy = gaze[:, 0], gaze[:, 1]
print("Gültige Werte (gx):", np.sum(~np.isnan(gx)), "von", len(gx))
print("Range:", np.nanmin(gx), np.nanmax(gx))
print("Gültige Werte (gy):", np.sum(~np.isnan(gy)), "von", len(gy))
print("Range:", np.nanmin(gy), np.nanmax(gy))
# Pupille
pupil, pupil_validity = extract_pupil(df)
features = []
# Sliding windows
for start in range(0, len(df), W):
end = start + W
if end > len(df):
break #das letzte Fenster wird ignoriert
w_gaze = gaze[start:end]
w_pupil = pupil[start:end]
w_valid = pupil_validity[start:end]
# ----------------------------
# FIXATIONS (PyGaze)
# ----------------------------
time_ms = np.arange(W) * 1000.0 / fs
# print("gx im Fenster:", w_gaze[:,0][:20])
# print("gy im Fenster:", w_gaze[:,1][:20])
# print("gx diff:", np.mean(np.abs(np.diff(w_gaze[:,0]))))
# print("Werte X im Fenster:", w_gaze[:,0])
# print("Werte Y im Fenster:", w_gaze[:,1])
# print("X-Stats: min/max/diff", np.nanmin(w_gaze[:,0]), np.nanmax(w_gaze[:,0]), np.nanmean(np.abs(np.diff(w_gaze[:,0]))))
# print("Y-Stats: min/max/diff", np.nanmin(w_gaze[:,1]), np.nanmax(w_gaze[:,1]), np.nanmean(np.abs(np.diff(w_gaze[:,1]))))
print("time_ms:", time_ms)
fix, efix = fixation_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms,
missing=0.0, maxdist=0.001, mindur=65 # mindur=100ms
)
#print("Raw Fixation Output:", efix[0])
if start == 0:
print("DEBUG fix raw:", fix[:10])
# Robust fixations: PyGaze may return malformed entries
fixation_durations = []
for f in efix:
print("Efix:", f[2])
# start_t = f[1] # in ms
# end_t = f[2] # in ms
# duration = (end_t - start_t) / 1000.0 # in Sekunden
#duration = f[2] / 1000.0
if np.isfinite(f[2]) and f[2] > 0:
fixation_durations.append(f[2])
# Kategorien laut Paper
F_short = sum(66 <= d <= 150 for d in fixation_durations)
F_medium = sum(300 <= d <= 500 for d in fixation_durations)
F_long = sum(d >= 1000 for d in fixation_durations)
F_hundred = sum(d > 100 for d in fixation_durations)
F_Cancel = sum(66 < d for d in fixation_durations)
# ----------------------------
# SACCADES
# ----------------------------
sac, esac = saccade_detection(
x=w_gaze[:, 0], y=w_gaze[:, 1], time=time_ms, missing=0, minlen=12, maxvel=0.2, maxacc=1
)
sac_durations = [s[2] for s in esac]
sac_amplitudes = [((s[5]-s[3])**2 + (s[6]-s[4])**2)**0.5 for s in esac]
# ----------------------------
# BLINKS
# ----------------------------
blinks = detect_blinks(w_valid)
blink_durations = [(b[1] - b[0]) / fs for b in blinks]
# ----------------------------
# PUPIL
# ----------------------------
if np.all(np.isnan(w_pupil)):
mean_pupil = np.nan
ipa = np.nan
else:
mean_pupil = np.nanmean(w_pupil)
ipa = compute_IPA(w_pupil, fs=fs)
# ----------------------------
# FEATURE-TABELLE FÜLLEN
# ----------------------------
features.append({
"Fix_count_short_66_150": F_short,
"Fix_count_medium_300_500": F_medium,
"Fix_count_long_gt_1000": F_long,
"Fix_count_100": F_hundred,
"Fix_cancel": F_Cancel,
"Fix_mean_duration": np.mean(fixation_durations) if fixation_durations else 0,
"Fix_median_duration": np.median(fixation_durations) if fixation_durations else 0,
"Sac_count": len(sac),
"Sac_mean_amp": np.mean(sac_amplitudes) if sac_amplitudes else 0,
"Sac_mean_dur": np.mean(sac_durations) if sac_durations else 0,
"Sac_median_dur": np.median(sac_durations) if sac_durations else 0,
"Blink_count": len(blinks),
"Blink_mean_dur": np.mean(blink_durations) if blink_durations else 0,
"Blink_median_dur": np.median(blink_durations) if blink_durations else 0,
"Pupil_mean": mean_pupil,
"Pupil_IPA": ipa
})
result = pd.DataFrame(features)
print("→ extract_eye_features(): Fertig! Ergebnisgröße:", result.shape)
return result
##############################################################################
# 3. MAIN FUNKTION
##############################################################################
def main():
print("### STARTE FEATURE-EXTRAKTION ###")
print("Aktueller Arbeitsordner:", os.getcwd())
df = pd.read_hdf("tmp22.h5", "SIGNALS", mode="r")
#df = pd.read_parquet("cleaned_0001.parquet")
print("DataFrame geladen:", df.shape)
# Nur Eye-Tracking auswählen
#eye_cols = [c for c in df.columns if "EYE_" in c]
#df_eye = df[eye_cols]
#print("Eye-Tracking-Spalten:", len(eye_cols))
#print("→", eye_cols[:10], " ...")
print("Reinige Eyetracking-Daten ...")
df_eye = clean_eye_df(df)
# Feature Extraction
features = extract_eye_features(df_eye, window_length_sec=50, fs=250)
print("\n### FEATURE-MATRIX (HEAD) ###")
print(features.head())
print("\nSpeichere Output in features.csv ...")
features.to_csv("features2.csv", index=False)
print("FERTIG!")
if __name__ == "__main__":
main()
+53 -46
View File
@@ -1,72 +1,79 @@
import math
def fixation_radius_normalized(theta_deg: float,
distance_cm: float,
screen_width_cm: float,
screen_height_cm: float,
resolution_x: int,
resolution_y: int,
method: str = "max"):
def fixation_radius_normalized(
theta_deg: float,
distance_cm: float,
screen_width_cm: float,
screen_height_cm: float,
resolution_x: int,
resolution_y: int,
method: str = "max",
):
"""
Berechnet den PyGaze-Fixationsradius für normierte Gaze-Daten in [0,1].
Compute the PyGaze fixation radius for normalized gaze data in [0, 1].
"""
# Schritt 1: visueller Winkel → physische Distanz (cm)
# Visual angle to physical distance (cm)
delta_cm = 2 * distance_cm * math.tan(math.radians(theta_deg) / 2)
# Schritt 2: physische Distanz → Pixel
# Physical distance to pixels
delta_px_x = delta_cm * (resolution_x / screen_width_cm)
delta_px_y = delta_cm * (resolution_y / screen_height_cm)
# Pixelradius
# Pixel radius
if method == "max":
r_px = max(delta_px_x, delta_px_y)
else:
r_px = math.sqrt(delta_px_x**2 + delta_px_y**2)
# Schritt 3: Pixelradius → normierter Radius
# Pixel radius to normalized radius
r_norm_x = r_px / resolution_x
r_norm_y = r_px / resolution_y
if method == "max":
return max(r_norm_x, r_norm_y)
else:
return math.sqrt(r_norm_x**2 + r_norm_y**2)
return math.sqrt(r_norm_x**2 + r_norm_y**2)
def run_example():
# Example: 55" 4k monitor
screen_width_cm = 3 * 121.8
screen_height_cm = 68.5
resolution_x = 3 * 3840
resolution_y = 2160
distance_to_screen_cm = 120
max_angle = 1.0
maxdist_px = fixation_radius_normalized(
theta_deg=max_angle,
distance_cm=distance_to_screen_cm,
screen_width_cm=screen_width_cm,
screen_height_cm=screen_height_cm,
resolution_x=resolution_x,
resolution_y=resolution_y,
method="max",
)
print("PyGaze max_dist (max):", maxdist_px)
maxdist_px = fixation_radius_normalized(
theta_deg=max_angle,
distance_cm=distance_to_screen_cm,
screen_width_cm=screen_width_cm,
screen_height_cm=screen_height_cm,
resolution_x=resolution_x,
resolution_y=resolution_y,
method="euclid",
)
print("PyGaze max_dist (euclid):", maxdist_px)
def main():
run_example()
# Beispiel: 55" 4k Monitor
screen_width_cm = 3*121.8
screen_height_cm = 68.5
resolution_x = 3*3840
resolution_y = 2160
distance_to_screen_cm = 120
method = 'max'
max_angle= 1.0
if __name__ == "__main__":
main()
maxdist_px = fixation_radius_normalized(theta_deg=max_angle,
distance_cm=distance_to_screen_cm,
screen_width_cm=screen_width_cm,
screen_height_cm=screen_height_cm,
resolution_x=resolution_x,
resolution_y=resolution_y,
method=method)
print("PyGaze max_dist (max):", maxdist_px)
method = 'euclid'
maxdist_px = fixation_radius_normalized(theta_deg=max_angle,
distance_cm=distance_to_screen_cm,
screen_width_cm=screen_width_cm,
screen_height_cm=screen_height_cm,
resolution_x=resolution_x,
resolution_y=resolution_y,
method=method)
print("PyGaze max_dist (euclid):", maxdist_px)
# Passt noch nicht zu der Breite
# https://osdoc.cogsci.nl/4.0/de/visualangle/
# https://reference.org/facts/Visual_angle/LUw29zy7
# Reference
# https://osdoc.cogsci.nl/4.0/de/visualangle/
# https://reference.org/facts/Visual_angle/LUw29zy7
-155
View File
@@ -1,155 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "2b3fface",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "74f1f5ec",
"metadata": {},
"outputs": [],
"source": [
"df= pd.read_parquet(r\" \")\n",
"print(df.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "05775454",
"metadata": {},
"outputs": [],
"source": [
"df.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "99e17328",
"metadata": {},
"outputs": [],
"source": [
"df.tail()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "69e53731",
"metadata": {},
"outputs": [],
"source": [
"df.info()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3754c664",
"metadata": {},
"outputs": [],
"source": [
"# Zeigt alle Kombinationen mit Häufigkeit\n",
"df[['STUDY', 'PHASE', 'LEVEL']].value_counts(ascending=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f83b595c",
"metadata": {},
"outputs": [],
"source": [
"high_nback = df[\n",
" (df[\"STUDY\"]==\"n-back\") &\n",
" (df[\"LEVEL\"].isin([2, 3, 5, 6])) &\n",
" (df[\"PHASE\"].isin([\"train\", \"test\"]))\n",
"]\n",
"high_nback.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c0940343",
"metadata": {},
"outputs": [],
"source": [
"low_all = df[\n",
" ((df[\"PHASE\"] == \"baseline\") |\n",
" ((df[\"STUDY\"] == \"n-back\") & (df[\"PHASE\"] != \"baseline\") & (df[\"LEVEL\"].isin([1,4]))))\n",
"]\n",
"print(low_all.shape)\n",
"high_kdrive = df[\n",
" (df[\"STUDY\"] == \"k-drive\") & (df[\"PHASE\"] != \"baseline\")\n",
"]\n",
"print(high_kdrive.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f7ce38d3",
"metadata": {},
"outputs": [],
"source": [
"print((df.shape[0]==(high_kdrive.shape[0]+high_nback.shape[0]+low_all.shape[0])))\n",
"print(df.shape[0])\n",
"print((high_kdrive.shape[0]+high_nback.shape[0]+low_all.shape[0]))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "48ba0379",
"metadata": {},
"outputs": [],
"source": [
"high_all = pd.concat([high_nback, high_kdrive])\n",
"high_all.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "77dda26c",
"metadata": {},
"outputs": [],
"source": [
"print(f\"Gesamt: {df.shape[0]}=={low_all.shape[0]+high_all.shape[0]}\")\n",
"print(f\"Anzahl an low load Samples: {low_all.shape[0]}\")\n",
"print(f\"Anzahl an high load Samples: {high_all.shape[0]}\")\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "base",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -1,8 +1,10 @@
import os
import pandas as pd
from pathlib import Path
# TODO: Set paths correctly
data_dir = Path("") # path to the directory with all .h5 files
base_dir = Path(r"") # directory to store the parquet files in
data_dir = Path("/home/jovyan/data-paulusjafahrsimulator-gpu/raw_data")
# Get all .h5 files and sort them
matching_files = sorted(data_dir.glob("*.h5"))
@@ -11,8 +13,8 @@ matching_files = sorted(data_dir.glob("*.h5"))
CHUNK_SIZE = 50_000
for i, file_path in enumerate(matching_files):
print(f"Subject {i} gestartet")
print(f"{file_path} geoeffnet")
print(f"Starting with subject {i}")
print(f"Opened: {file_path}")
# Step 1: Get total number of rows and column names
with pd.HDFStore(file_path, mode="r") as store:
@@ -81,7 +83,7 @@ for i, file_path in enumerate(matching_files):
print(f"Final dataframe shape: {df_final.shape}")
# Save to parquet
base_dir = Path(r"/home/jovyan/data-paulusjafahrsimulator-gpu/both_mod_parquet_files")
os.makedirs(base_dir, exist_ok=True)
out_name = base_dir / f"both_mod_{i:04d}.parquet"
Binary file not shown.
+61 -282
View File
@@ -41,7 +41,6 @@
"import time\n",
"base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n",
"sys.path.append(base_dir)\n",
"print(base_dir)\n",
"\n",
"from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler, mad_outlier_removal, performance_split\n",
"from sklearn.preprocessing import StandardScaler, MinMaxScaler\n",
@@ -51,7 +50,7 @@
"import tensorflow as tf\n",
"from tensorflow.keras import layers, models, regularizers\n",
"import pickle\n",
"from sklearn.metrics import (roc_auc_score, accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report, balanced_accuracy_score, ConfusionMatrixDisplay, auc, roc_curve) "
"from sklearn.metrics import (accuracy_score, auc, roc_curve, f1_score) "
]
},
{
@@ -89,15 +88,40 @@
"id": "f00a477c",
"metadata": {},
"source": [
"### Data Preprocessing"
"### Configuration of paths and data preprocessing"
]
},
{
"cell_type": "markdown",
"id": "504c1df7",
"cell_type": "code",
"execution_count": null,
"id": "5136fcec",
"metadata": {},
"outputs": [],
"source": [
"Laden der Daten"
"# TODO: set path where to save normalizer\n",
"normalizer_path=Path('.pkl') # TODO: set manually"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2115f65",
"metadata": {},
"outputs": [],
"source": [
"performance_path = Path(r\".csv\") # TODO: set manually\n",
"performance_df = pd.read_csv(performance_path)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "559eb8d2",
"metadata": {},
"outputs": [],
"source": [
"encoder_save_path = Path('.keras') # TODO: set manually\n",
"deep_svdd_save_path = Path('.keras') # TODO: set manually"
]
},
{
@@ -107,8 +131,7 @@
"metadata": {},
"outputs": [],
"source": [
"dataset_path = Path(r\"data-paulusjafahrsimulator-gpu/new_datasets/combined_dataset_25hz.parquet\")\n",
"# dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/new_datasets/120s_combined_dataset_25hz.parquet\")"
"dataset_path = Path(r\".parquet\") # TODO: set manually"
]
},
{
@@ -121,17 +144,6 @@
"df = pd.read_parquet(path=dataset_path)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2115f65",
"metadata": {},
"outputs": [],
"source": [
"performance_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/subject_performance/3new_au_performance.csv\")\n",
"performance_df = pd.read_csv(performance_path)"
]
},
{
"cell_type": "markdown",
"id": "c045c46d",
@@ -215,7 +227,7 @@
"\n",
"data = pd.concat([low, high], ignore_index=True)\n",
"df = data.drop_duplicates()\n",
"\n",
"df = df.dropna()\n",
"print(\"Label distribution:\")\n",
"print(df[\"label\"].value_counts())"
]
@@ -275,210 +287,6 @@
"Normalization"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "acec4a03",
"metadata": {},
"outputs": [],
"source": [
"def fit_normalizer(train_data, au_columns, method='standard', scope='global'):\n",
" \"\"\"\n",
" Fit normalization scalers on training data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" train_data : pd.DataFrame\n",
" Training dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" method : str, default='standard'\n",
" Normalization method: 'standard' for StandardScaler or 'minmax' for MinMaxScaler\n",
" scope : str, default='global'\n",
" Normalization scope: 'subject' for per-subject or 'global' for across all subjects\n",
" \n",
" Returns:\n",
" --------\n",
" dict\n",
" Dictionary containing fitted scalers and statistics for new subjects\n",
" \"\"\"\n",
" if method == 'standard':\n",
" Scaler = StandardScaler\n",
" elif method == 'minmax':\n",
" Scaler = MinMaxScaler\n",
" else:\n",
" raise ValueError(\"method must be 'standard' or 'minmax'\")\n",
" \n",
" scalers = {}\n",
" if scope == 'subject':\n",
" # Fit one scaler per subject\n",
" subject_stats = []\n",
" \n",
" for subject in train_data['subjectID'].unique():\n",
" subject_mask = train_data['subjectID'] == subject\n",
" scaler = Scaler()\n",
" scaler.fit(train_data.loc[subject_mask, au_columns].values)\n",
" scalers[subject] = scaler\n",
" \n",
" # Store statistics for averaging\n",
" if method == 'standard':\n",
" subject_stats.append({\n",
" 'mean': scaler.mean_,\n",
" 'std': scaler.scale_\n",
" })\n",
" elif method == 'minmax':\n",
" subject_stats.append({\n",
" 'min': scaler.data_min_,\n",
" 'max': scaler.data_max_\n",
" })\n",
" \n",
" # Calculate average statistics for new subjects\n",
" if method == 'standard':\n",
" avg_mean = np.mean([s['mean'] for s in subject_stats], axis=0)\n",
" avg_std = np.mean([s['std'] for s in subject_stats], axis=0)\n",
" fallback_scaler = StandardScaler()\n",
" fallback_scaler.mean_ = avg_mean\n",
" fallback_scaler.scale_ = avg_std\n",
" fallback_scaler.var_ = avg_std ** 2\n",
" fallback_scaler.n_features_in_ = len(au_columns)\n",
" elif method == 'minmax':\n",
" avg_min = np.mean([s['min'] for s in subject_stats], axis=0)\n",
" avg_max = np.mean([s['max'] for s in subject_stats], axis=0)\n",
" fallback_scaler = MinMaxScaler()\n",
" fallback_scaler.data_min_ = avg_min\n",
" fallback_scaler.data_max_ = avg_max\n",
" fallback_scaler.data_range_ = avg_max - avg_min\n",
" fallback_scaler.scale_ = 1.0 / fallback_scaler.data_range_\n",
" fallback_scaler.min_ = -avg_min * fallback_scaler.scale_\n",
" fallback_scaler.n_features_in_ = len(au_columns)\n",
" \n",
" scalers['_fallback'] = fallback_scaler\n",
" \n",
" elif scope == 'global':\n",
" # Fit one scaler for all subjects\n",
" scaler = Scaler()\n",
" scaler.fit(train_data[au_columns].values)\n",
" scalers['global'] = scaler\n",
" \n",
" else:\n",
" raise ValueError(\"scope must be 'subject' or 'global'\")\n",
" \n",
" return {'scalers': scalers, 'method': method, 'scope': scope}\n",
"\n",
"def apply_normalizer(data, columns, normalizer_dict):\n",
" \"\"\"\n",
" Apply fitted normalization scalers to data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" data : pd.DataFrame\n",
" Dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" normalizer_dict : dict\n",
" Dictionary containing fitted scalers from fit_normalizer()\n",
" \n",
" Returns:\n",
" --------\n",
" pd.DataFrame\n",
" DataFrame with normalized AU columns\n",
" \"\"\"\n",
" normalized_data = data.copy()\n",
" scalers = normalizer_dict['scalers']\n",
" scope = normalizer_dict['scope']\n",
" normalized_data[columns] = normalized_data[columns].astype(np.float64)\n",
"\n",
" if scope == 'subject':\n",
" # Apply per-subject normalization\n",
" for subject in data['subjectID'].unique():\n",
" subject_mask = data['subjectID'] == subject\n",
" \n",
" # Use the subject's scaler if available, otherwise use fallback\n",
" if subject in scalers:\n",
" scaler = scalers[subject]\n",
" else:\n",
" # Use averaged scaler for new subjects\n",
" scaler = scalers['_fallback']\n",
" print(f\"Info: Subject {subject} not in training data. Using averaged scaler from training subjects.\")\n",
" \n",
" normalized_data.loc[subject_mask, columns] = scaler.transform(\n",
" data.loc[subject_mask, columns].values\n",
" )\n",
" \n",
" elif scope == 'global':\n",
" # Apply global normalization\n",
" scaler = scalers['global']\n",
" normalized_data[columns] = scaler.transform(data[columns].values)\n",
" \n",
" return normalized_data\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "53c6ee6f",
"metadata": {},
"outputs": [],
"source": [
"def save_normalizer(normalizer_dict, filepath):\n",
" \"\"\"\n",
" Save fitted normalizer to disk.\n",
"\n",
" Parameters:\n",
" -----------\n",
" normalizer_dict : dict\n",
" Dictionary containing fitted scalers from fit_normalizer()\n",
" filepath : str\n",
" Path to save the normalizer (e.g., 'normalizer.pkl')\n",
" \"\"\"\n",
" # Create directory if it does not exist\n",
" dirpath = os.path.dirname(filepath)\n",
" if dirpath:\n",
" os.makedirs(dirpath, exist_ok=True)\n",
"\n",
" with open(filepath, 'wb') as f:\n",
" pickle.dump(normalizer_dict, f)\n",
"\n",
" print(f\"Normalizer saved to {filepath}\")\n",
"\n",
"def load_normalizer(filepath):\n",
" \"\"\"\n",
" Load fitted normalizer from disk.\n",
" \n",
" Parameters:\n",
" -----------\n",
" filepath : str\n",
" Path to the saved normalizer file\n",
" \n",
" Returns:\n",
" --------\n",
" dict\n",
" Dictionary containing fitted scalers\n",
" \"\"\"\n",
" with open(filepath, 'rb') as f:\n",
" normalizer_dict = pickle.load(f)\n",
" print(f\"Normalizer loaded from {filepath}\")\n",
" return normalizer_dict"
]
},
{
"cell_type": "markdown",
"id": "7280f64f",
"metadata": {},
"source": [
"save Normalizer"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8420afc2",
"metadata": {},
"outputs": [],
"source": [
"normalizer_path=Path('data-paulusjafahrsimulator-gpu/saved_models/deepsvdd_save/normalizer_min_max_global.pkl')"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -495,8 +303,10 @@
"print(len(eye_cols))\n",
"all_signal_columns = face_au_cols+eye_cols\n",
"print(len(all_signal_columns))\n",
"normalizer = fit_normalizer(train_df, all_signal_columns, method='minmax', scope='global')\n",
"save_normalizer(normalizer, normalizer_path )"
"\n",
"# fit and save normalizer\n",
"normalizer = scaler.fit_normalizer(train_df, all_signal_columns, method='minmax', scope='global')\n",
"scaler.save_normalizer(normalizer, normalizer_path )"
]
},
{
@@ -506,11 +316,11 @@
"metadata": {},
"outputs": [],
"source": [
"normalizer = load_normalizer(normalizer_path)\n",
"# 3. Apply normalization to all sets\n",
"train_df_norm = apply_normalizer(train_df, all_signal_columns, normalizer)\n",
"val_df_norm = apply_normalizer(val_df, all_signal_columns, normalizer)\n",
"test_df_norm = apply_normalizer(test_df, all_signal_columns, normalizer)"
"normalizer = scaler.load_normalizer(normalizer_path)\n",
"# Apply normalization to all sets\n",
"train_df_norm = scaler.apply_normalizer(train_df, all_signal_columns, normalizer)\n",
"val_df_norm = scaler.apply_normalizer(val_df, all_signal_columns, normalizer)\n",
"test_df_norm = scaler.apply_normalizer(test_df, all_signal_columns, normalizer)"
]
},
{
@@ -566,13 +376,13 @@
"def build_intermediate_fusion_autoencoder(\n",
" input_dim_mod1=15,\n",
" input_dim_mod2=20,\n",
" encoder_hidden_dim_mod1=12, # individuell\n",
" encoder_hidden_dim_mod2=20, # individuell\n",
" latent_dim=6, # Änderung: Bottleneck vergrößert für stabilere Repräsentation\n",
" dropout_rate=0.4, # Dropout in Hidden Layers\n",
" neg_slope=0.1,\n",
" weight_decay=1e-4,\n",
" decoder_hidden_dims=[16, 32] # Änderung: Decoder größer für bessere Rekonstruktion\n",
" encoder_hidden_dim_mod1=12, # TODO: set manually\n",
" encoder_hidden_dim_mod2=20, # TODO: set manually\n",
" latent_dim=6, # TODO: set manually\n",
" dropout_rate=0.4, # TODO: set manually\n",
" neg_slope=0.1, # TODO: set manually\n",
" weight_decay=1e-4, # TODO: set manually\n",
" decoder_hidden_dims=[16, 32] # TODO: set manually\n",
"):\n",
" \"\"\"\n",
" Verbesserter Intermediate-Fusion Autoencoder für Deep SVDD.\n",
@@ -597,10 +407,10 @@
" kernel_regularizer=l2\n",
" )(x1_in)\n",
" e1 = act(e1)\n",
" e1 = layers.Dropout(dropout_rate)(e1) # Dropout nur hier\n",
" e1 = layers.Dropout(dropout_rate)(e1) \n",
"\n",
" e1 = layers.Dense(\n",
" 16, # Änderung: Hidden Layer größer für stabilere Fusion\n",
" 16, \n",
" use_bias=False,\n",
" kernel_regularizer=l2\n",
" )(e1)\n",
@@ -613,20 +423,20 @@
" kernel_regularizer=l2\n",
" )(x2_in)\n",
" e2 = act(e2)\n",
" e2 = layers.Dropout(dropout_rate)(e2) # Dropout nur hier\n",
" e2 = layers.Dropout(dropout_rate)(e2) \n",
"\n",
" e2 = layers.Dense(\n",
" 16, # Änderung: Hidden Layer größer\n",
" 16, \n",
" use_bias=False,\n",
" kernel_regularizer=l2\n",
" )(e2)\n",
" e2 = act(e2)\n",
"\n",
" # -------- Intermediate Fusion --------\n",
" fused = layers.Concatenate(name=\"fusion\")([e1, e2]) # 16+16=32 Dimensionen\n",
" fused = layers.Concatenate(name=\"fusion\")([e1, e2]) # 16+16=32 dimensions\n",
"\n",
" # -------- Joint Encoder / Bottleneck --------\n",
" # sinnvoll kleiner als Fusion\n",
"\n",
" h = layers.Dense(\n",
" latent_dim,\n",
" use_bias=False,\n",
@@ -637,16 +447,16 @@
"\n",
" z = layers.Dense(\n",
" latent_dim,\n",
" activation=None, # linear, für Deep SVDD\n",
" activation=None, # linear for Deep SVDD\n",
" use_bias=False,\n",
" kernel_regularizer=l2,\n",
" name=\"latent\"\n",
" )(h)\n",
" # Dropout entfernt direkt vor Bottleneck\n",
"\n",
"\n",
" # -------- Decoder --------\n",
" d = layers.Dense(\n",
" decoder_hidden_dims[0], # größerer Decoder\n",
" decoder_hidden_dims[0], \n",
" use_bias=False,\n",
" kernel_regularizer=l2\n",
" )(z)\n",
@@ -692,10 +502,10 @@
"model = build_intermediate_fusion_autoencoder(\n",
" input_dim_mod1=len(face_au_cols),\n",
" input_dim_mod2=len(eye_cols),\n",
" encoder_hidden_dim_mod1=12, # individuell\n",
" encoder_hidden_dim_mod2=8, # individuell\n",
" encoder_hidden_dim_mod1=12, # TODO: set manually\n",
" encoder_hidden_dim_mod2=8, # TODO: set manually\n",
" latent_dim=4,\n",
" dropout_rate=0.7, # einstellbar\n",
" dropout_rate=0.7, # TODO: set manually\n",
" neg_slope=0.1,\n",
" weight_decay=1e-3\n",
")\n",
@@ -780,7 +590,6 @@
"metadata": {},
"outputs": [],
"source": [
"encoder_save_path =Path('data-paulusjafahrsimulator-gpu/saved_models/deepsvdd_save/encoder_8_deep.keras')\n",
"encoder.save(encoder_save_path)"
]
},
@@ -876,22 +685,6 @@
"center = get_center(deep_svdd_net, [X_face, X_eye])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "da140072",
"metadata": {},
"outputs": [],
"source": [
"# def get_radius(nu, dataset):\n",
"# x_face, x_eye = dataset # <-- zwingend entpacken\n",
"\n",
"# dataset_tuple=[x_face, x_eye]\n",
"\n",
"# dists = dist_per_sample(deep_svdd_net.predict(dataset_tuple), center)\n",
"# return np.quantile(np.sqrt(dists), 1-nu).astype(np.float32)"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -944,10 +737,9 @@
" return get_radius_from_arrays(nu, X_face, X_eye)\n",
"\n",
"\n",
"nu = 0.25\n",
"nu = 0.05 # Set nu respectively\n",
"\n",
"train_dataset = tf.data.Dataset.from_tensor_slices((X_face, X_eye)).shuffle(64).batch(64)\n",
"# train_dataset = tf.data.Dataset.from_tensor_slices((X_face, X_eye))\n",
"\n",
"optimizer = tf.keras.optimizers.Adam(1e-3)\n",
"train(train_dataset, epochs=150, nu=nu)\n",
@@ -1019,7 +811,6 @@
"metadata": {},
"outputs": [],
"source": [
"deep_svdd_save_path =Path('data-paulusjafahrsimulator-gpu/saved_models/deepsvdd_save/deep_svdd_06.keras')\n",
"deep_svdd_net.save(deep_svdd_save_path)"
]
},
@@ -1104,18 +895,6 @@
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.10"
}
},
"nbformat": 4,
@@ -28,7 +28,7 @@
"sys.path.append(base_dir)\n",
"print(base_dir)\n",
"\n",
"from tools import evaluation_tools\n",
"from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler\n",
"from sklearn.preprocessing import StandardScaler, MinMaxScaler\n",
"from sklearn.ensemble import IsolationForest\n",
"from sklearn.model_selection import GridSearchCV, KFold\n",
@@ -52,7 +52,7 @@
"metadata": {},
"outputs": [],
"source": [
"data_path = Path(r\"C:\\Users\\micha\\FAUbox\\WS2526_Fahrsimulator_MSY (Celina Korzer)\\AU_dataset\\output_windowed.parquet\")"
"data_path = Path(r\".parquet\") # TODO: set manually"
]
},
{
@@ -115,118 +115,6 @@
"print(f\"high all: {high_all.shape}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "47a0f44d",
"metadata": {},
"outputs": [],
"source": [
"def fit_normalizer(train_data, au_columns, method='standard', scope='global'):\n",
" \"\"\"\n",
" Fit normalization scalers on training data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" train_data : pd.DataFrame\n",
" Training dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" method : str, default='standard'\n",
" Normalization method: 'standard' for StandardScaler or 'minmax' for MinMaxScaler\n",
" scope : str, default='global'\n",
" Normalization scope: 'subject' for per-subject or 'global' for across all subjects\n",
" \n",
" Returns:\n",
" --------\n",
" dict\n",
" Dictionary containing fitted scalers\n",
" \"\"\"\n",
" # Select scaler based on method\n",
" if method == 'standard':\n",
" Scaler = StandardScaler\n",
" elif method == 'minmax':\n",
" Scaler = MinMaxScaler\n",
" else:\n",
" raise ValueError(\"method must be 'standard' or 'minmax'\")\n",
" \n",
" scalers = {}\n",
" \n",
" if scope == 'subject':\n",
" # Fit one scaler per subject\n",
" for subject in train_data['subjectID'].unique():\n",
" subject_mask = train_data['subjectID'] == subject\n",
" scaler = Scaler()\n",
" scaler.fit(train_data.loc[subject_mask, au_columns])\n",
" scalers[subject] = scaler\n",
" \n",
" elif scope == 'global':\n",
" # Fit one scaler for all subjects\n",
" scaler = Scaler()\n",
" scaler.fit(train_data[au_columns])\n",
" scalers['global'] = scaler\n",
" \n",
" else:\n",
" raise ValueError(\"scope must be 'subject' or 'global'\")\n",
" \n",
" return {'scalers': scalers, 'method': method, 'scope': scope}"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "642d0017",
"metadata": {},
"outputs": [],
"source": [
"def apply_normalizer(data, au_columns, normalizer_dict):\n",
" \"\"\"\n",
" Apply fitted normalization scalers to data.\n",
" \n",
" Parameters:\n",
" -----------\n",
" data : pd.DataFrame\n",
" Dataframe with AU columns and subjectID\n",
" au_columns : list\n",
" List of AU column names to normalize\n",
" normalizer_dict : dict\n",
" Dictionary containing fitted scalers from fit_normalizer()\n",
" \n",
" Returns:\n",
" --------\n",
" pd.DataFrame\n",
" DataFrame with normalized AU columns\n",
" \"\"\"\n",
" normalized_data = data.copy()\n",
" scalers = normalizer_dict['scalers']\n",
" scope = normalizer_dict['scope']\n",
" \n",
" if scope == 'subject':\n",
" # Apply per-subject normalization\n",
" for subject in data['subjectID'].unique():\n",
" subject_mask = data['subjectID'] == subject\n",
" \n",
" # Use the subject's scaler if available, otherwise use a fitted scaler from training\n",
" if subject in scalers:\n",
" scaler = scalers[subject]\n",
" else:\n",
" # For new subjects not seen in training, use the first available scaler\n",
" # (This is a fallback - ideally all test subjects should be in training for subject-level normalization)\n",
" print(f\"Warning: Subject {subject} not found in training data. Using fallback scaler.\")\n",
" scaler = list(scalers.values())[0]\n",
" \n",
" normalized_data.loc[subject_mask, au_columns] = scaler.transform(\n",
" data.loc[subject_mask, au_columns]\n",
" )\n",
" \n",
" elif scope == 'global':\n",
" # Apply global normalization\n",
" scaler = scalers['global']\n",
" normalized_data[au_columns] = scaler.transform(data[au_columns])\n",
" \n",
" return normalized_data"
]
},
{
"cell_type": "markdown",
"id": "697b3cf7",
@@ -301,20 +189,26 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 2: Get AU columns and prepare datasets\n",
"# Get all column names that start with 'AU'\n",
"au_columns = [col for col in low_all.columns if col.startswith('AU')]\n",
"au_columns = [col for col in low_all.columns if \"face\" in col.lower()] \n",
"\n",
"eye_columns = [ \n",
" 'Fix_count_short_66_150','Fix_count_medium_300_500','Fix_count_long_gt_1000', \n",
" 'Fix_count_100','Fix_mean_duration','Fix_median_duration', \n",
" 'Sac_count','Sac_mean_amp','Sac_mean_dur','Sac_median_dur', \n",
" 'Blink_count','Blink_mean_dur','Blink_median_dur', \n",
" 'Pupil_mean','Pupil_IPA' \n",
"] \n",
"cols = au_columns +eye_columns\n",
"# Prepare training data (only normal/low data)\n",
"train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + au_columns].copy()\n",
"train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + cols].copy()\n",
"\n",
"# Prepare validation data (normal and anomaly)\n",
"val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n",
"val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n",
"val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"\n",
"# Prepare test data (normal and anomaly)\n",
"test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n",
"test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n",
"test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"\n",
"print(f\"Train samples: {len(train_data)}\")\n",
"print(f\"Val normal samples: {len(val_normal_data)}, Val high samples: {len(val_high_data)}\")\n",
@@ -328,8 +222,8 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 3: Fit normalizer on training data\n",
"normalizer = fit_normalizer(train_data, au_columns, method='minmax', scope='global')\n",
"# Fit normalizer on training data\n",
"normalizer = scaler.fit_normalizer(train_data, cols, method='minmax', scope='global')\n",
"print(\"Normalizer fitted on training data\")"
]
},
@@ -340,12 +234,12 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 4: Apply normalization to all datasets\n",
"train_normalized = apply_normalizer(train_data, au_columns, normalizer)\n",
"val_normal_normalized = apply_normalizer(val_normal_data, au_columns, normalizer)\n",
"val_high_normalized = apply_normalizer(val_high_data, au_columns, normalizer)\n",
"test_normal_normalized = apply_normalizer(test_normal_data, au_columns, normalizer)\n",
"test_high_normalized = apply_normalizer(test_high_data, au_columns, normalizer)\n",
"# Apply normalization to all datasets\n",
"train_normalized = scaler.apply_normalizer(train_data, cols, normalizer)\n",
"val_normal_normalized = scaler.apply_normalizer(val_normal_data, cols, normalizer)\n",
"val_high_normalized = scaler.apply_normalizer(val_high_data, cols, normalizer)\n",
"test_normal_normalized = scaler.apply_normalizer(test_normal_data, cols, normalizer)\n",
"test_high_normalized = scaler.apply_normalizer(test_high_data, cols, normalizer)\n",
"\n",
"print(\"Normalization applied to all datasets\")"
]
@@ -357,11 +251,9 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 5: Extract AU columns and create labels for grid search\n",
"# Extract only AU columns (drop subjectID)\n",
"X_train = train_normalized[au_columns].copy()\n",
"X_val_normal = val_normal_normalized[au_columns].copy()\n",
"X_val_high = val_high_normalized[au_columns].copy()\n",
"X_train = train_normalized[cols].copy()\n",
"X_val_normal = val_normal_normalized[cols].copy()\n",
"X_val_high = val_high_normalized[cols].copy()\n",
"\n",
"# Combine train and validation sets for grid search\n",
"X_grid_search = pd.concat([X_train, X_val_normal, X_val_high], ignore_index=True)\n",
@@ -416,7 +308,7 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 7: Train final model with best parameters on training data\n",
"# Train final model with best parameters on training data\n",
"final_model = IsolationForest(**best_params, random_state=42)\n",
"final_model.fit(X_train.values)\n",
"\n",
@@ -430,9 +322,9 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 8: Prepare independent test set\n",
"X_test_normal = test_normal_normalized[au_columns].copy()\n",
"X_test_high = test_high_normalized[au_columns].copy()\n",
"# Prepare independent test set\n",
"X_test_normal = test_normal_normalized[cols].copy()\n",
"X_test_high = test_high_normalized[cols].copy()\n",
"\n",
"# Combine test sets\n",
"X_test = pd.concat([X_test_normal, X_test_high], ignore_index=True)\n",
@@ -483,21 +375,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "base",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.5"
}
},
"nbformat": 4,
+102 -280
View File
@@ -20,10 +20,10 @@
"from pathlib import Path\n",
"import sys\n",
"import os\n",
"import tensorflow as tf\n",
"\n",
"base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n",
"sys.path.append(base_dir)\n",
"print(base_dir)\n",
"\n",
"from sklearn.pipeline import Pipeline\n",
"from sklearn.svm import OneClassSVM\n",
@@ -31,7 +31,7 @@
"import matplotlib.pyplot as plt\n",
"import tensorflow as tf\n",
"import pickle\n",
"from tools import evaluation_tools, scaler\n",
"from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler\n",
"from sklearn.metrics import (balanced_accuracy_score, accuracy_score, precision_score, \n",
" recall_score, f1_score, confusion_matrix, classification_report) "
]
@@ -44,6 +44,17 @@
"### Load data"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "30a4c885",
"metadata": {},
"outputs": [],
"source": [
"enconder_path = Path(\".keras\")\n",
"model_path = Path(\".pkl\")"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -51,7 +62,7 @@
"metadata": {},
"outputs": [],
"source": [
"data_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/first_AU_dataset/output_windowed.parquet\")"
"data_path = Path(r\".parquet\")"
]
},
{
@@ -61,7 +72,8 @@
"metadata": {},
"outputs": [],
"source": [
"df = pd.read_parquet(path=data_path)"
"df = pd.read_parquet(path=data_path)\n",
"df = df.dropna()"
]
},
{
@@ -150,20 +162,29 @@
"metadata": {},
"outputs": [],
"source": [
"au_columns = [col for col in low_all.columns if col.startswith('AU')]\n",
"au_columns = [col for col in low_all.columns if \"face\" in col.lower()] \n",
"\n",
"eye_columns = [ \n",
" 'Fix_count_short_66_150','Fix_count_medium_300_500','Fix_count_long_gt_1000', \n",
" 'Fix_count_100','Fix_mean_duration','Fix_median_duration', \n",
" 'Sac_count','Sac_mean_amp','Sac_mean_dur','Sac_median_dur', \n",
" 'Blink_count','Blink_mean_dur','Blink_median_dur', \n",
" 'Pupil_mean','Pupil_IPA' \n",
"] \n",
"cols = au_columns +eye_columns\n",
"\n",
"# Prepare training data (only normal/low data)\n",
"train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + au_columns].copy()\n",
"train_data = low_all[low_all['subjectID'].isin(train_subjects)][['subjectID'] + cols].copy()\n",
"\n",
"# Prepare validation data (normal and anomaly) \n",
"val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n",
"val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + au_columns].copy()\n",
"val_normal_data = val_normal_data.sample(n=1000, random_state=42)\n",
"val_high_data = val_high_data.sample(n=1000, random_state=42)\n",
"val_normal_data = low_all[low_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"val_high_data = high_all[high_all['subjectID'].isin(val_subjects)][['subjectID'] + cols].copy()\n",
"val_normal_data = val_normal_data.sample(n=500, random_state=42)\n",
"val_high_data = val_high_data.sample(n=500, random_state=42)\n",
"\n",
"# Prepare test data (normal and anomaly) - 1000 samples each\n",
"test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n",
"test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + au_columns].copy()\n",
"test_normal_data = low_all[low_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"test_high_data = high_all[high_all['subjectID'].isin(test_subjects)][['subjectID'] + cols].copy()\n",
"test_normal_data = test_normal_data.sample(n=500, random_state=42)\n",
"test_high_data = test_high_data.sample(n=500, random_state=42)\n",
"\n",
@@ -186,7 +207,7 @@
"outputs": [],
"source": [
"# Cell 3: Fit normalizer on training data\n",
"normalizer = scaler.fit_normalizer(train_data, au_columns, method='minmax', scope='global')\n",
"normalizer = scaler.fit_normalizer(train_data, cols, method='minmax', scope='global')\n",
"print(\"Normalizer fitted on training data\")"
]
},
@@ -198,11 +219,11 @@
"outputs": [],
"source": [
"# Cell 4: Apply normalization to all datasets\n",
"train_normalized = scaler.apply_normalizer(train_data, au_columns, normalizer)\n",
"val_normal_normalized = scaler.apply_normalizer(val_normal_data, au_columns, normalizer)\n",
"val_high_normalized = scaler.apply_normalizer(val_high_data, au_columns, normalizer)\n",
"test_normal_normalized = scaler.apply_normalizer(test_normal_data, au_columns, normalizer)\n",
"test_high_normalized = scaler.apply_normalizer(test_high_data, au_columns, normalizer)\n",
"train_normalized = scaler.apply_normalizer(train_data, cols, normalizer)\n",
"val_normal_normalized = scaler.apply_normalizer(val_normal_data, cols, normalizer)\n",
"val_high_normalized = scaler.apply_normalizer(val_high_data, cols, normalizer)\n",
"test_normal_normalized = scaler.apply_normalizer(test_normal_data, cols, normalizer)\n",
"test_high_normalized = scaler.apply_normalizer(test_high_data, cols, normalizer)\n",
"\n",
"print(\"Normalization applied to all datasets\")"
]
@@ -214,13 +235,11 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 5: Extract AU columns and create labels for grid search\n",
"# Extract only AU columns (drop subjectID)\n",
"X_train = train_normalized[au_columns].copy()\n",
"X_val_normal = val_normal_normalized[au_columns].copy()\n",
"X_val_high = val_high_normalized[au_columns].copy()\n",
"X_test_high = test_high_normalized[au_columns].copy()\n",
"X_test_normal = test_normal_normalized[au_columns].copy()\n",
"X_train = train_normalized[cols].copy()\n",
"X_val_normal = val_normal_normalized[cols].copy()\n",
"X_val_high = val_high_normalized[cols].copy()\n",
"X_test_high = test_high_normalized[cols].copy()\n",
"X_test_normal = test_normal_normalized[cols].copy()\n",
"\n",
"\n",
"# Create labels for grid search\n",
@@ -241,116 +260,12 @@
"X_train.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "50fc80dc-fe16-4917-aad6-0dbaa1ce5ef9",
"metadata": {},
"outputs": [],
"source": [
"!pip install keras-tuner --quiet # nur einmal nötig\n",
"\n",
"import tensorflow as tf\n",
"from tensorflow import keras\n",
"from kerastuner import HyperModel\n",
"from kerastuner.tuners import RandomSearch\n",
"\n",
"# 1️⃣ HyperModel definieren\n",
"class AutoencoderHyperModel(HyperModel):\n",
" def __init__(self, input_dim):\n",
" self.input_dim = input_dim\n",
"\n",
" def build(self, hp):\n",
" reg = hp.Float(\"l2_reg\", min_value=1e-5, max_value=0.01, sampling=\"log\")\n",
" lr = hp.Float(\"learning_rate\", 1e-4, 1e-2, sampling=\"log\")\n",
"\n",
" # Encoder\n",
" encoder = keras.Sequential([\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"enc_units1\", min_value=10, max_value=self.input_dim, step=10),\n",
" activation=None,\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.LeakyReLU(alpha=0.1),\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"enc_units2\", min_value=5, max_value=20, step=1),\n",
" activation=tf.keras.layers.LeakyReLU(alpha=0.1),\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.Dense(\n",
" units=2, # Bottleneck\n",
" activation='linear',\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" ])\n",
"\n",
" # Decoder\n",
" decoder = keras.Sequential([\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"dec_units1\", min_value=5, max_value=20, step=1),\n",
" activation=tf.keras.layers.LeakyReLU(alpha=0.1),\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.Dense(\n",
" units=hp.Int(\"dec_units2\", min_value=10, max_value=self.input_dim, step=10),\n",
" activation=tf.keras.layers.LeakyReLU(alpha=0.1),\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" keras.layers.Dense(\n",
" units=self.input_dim,\n",
" activation='linear',\n",
" kernel_regularizer=keras.regularizers.l2(reg)\n",
" ),\n",
" ])\n",
"\n",
" # Autoencoder\n",
" inputs = keras.Input(shape=(self.input_dim,))\n",
" encoded = encoder(inputs)\n",
" decoded = decoder(encoded)\n",
" autoencoder = keras.Model(inputs, decoded)\n",
"\n",
" autoencoder.compile(\n",
" optimizer=keras.optimizers.Adam(learning_rate=lr),\n",
" loss='mse'\n",
" )\n",
"\n",
" return autoencoder\n",
"\n",
"# 2️⃣ RandomSearch-Tuner\n",
"hypermodel = AutoencoderHyperModel(input_dim=X_train.shape[1])\n",
"\n",
"tuner = RandomSearch(\n",
" hypermodel,\n",
" objective='val_loss',\n",
" max_trials=10, # Anzahl der getesteten Kombinationen\n",
" executions_per_trial=1, # Anzahl Trainings pro Kombination\n",
" directory='tuner_dir',\n",
" project_name='oc_ae'\n",
")\n",
"\n",
"# 3️⃣ Hyperparameter-Tuning starten\n",
"tuner.search(\n",
" X_train, X_train,\n",
" epochs=100,\n",
" batch_size=64,\n",
" validation_data=(X_val_normal, X_val_normal),\n",
" verbose=0\n",
")\n",
"\n",
"# 4️⃣ Beste Architektur holen\n",
"best_model = tuner.get_best_models(num_models=1)[0]\n",
"best_hyperparameters = tuner.get_best_hyperparameters(1)[0]\n",
"\n",
"print(\"Beste Hyperparameter:\", best_hyperparameters.values)\n"
]
},
{
"cell_type": "markdown",
"id": "362c0a6f",
"metadata": {},
"source": [
"\n",
"Beste Hyperparameter: {'l2_reg': 1.3757411430582133e-05, 'learning_rate': 0.007321002854350309, 'enc_units1': 20, 'enc_units2': 16, 'dec_units1': 14, 'dec_units2': 10}"
"Build model"
]
},
{
@@ -360,26 +275,6 @@
"metadata": {},
"outputs": [],
"source": [
"# reg = 0.1\n",
"# encoder = tf.keras.Sequential(\n",
"# [\n",
"# tf.keras.layers.Dense(units=X_train.shape[1], activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=10, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=5, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
" \n",
"# ]\n",
"# )\n",
"\n",
"# decoder = tf.keras.Sequential(\n",
"# [\n",
"# tf.keras.layers.Dense(units=5,activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=10, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(reg)),\n",
"# tf.keras.layers.Dense(units=X_train.shape[1], activation='linear', kernel_regularizer=tf.keras.regularizers.l2(reg))\n",
"# ]\n",
"# )\n",
"\n",
"\n",
"\n",
"reg = 1e-5\n",
"\n",
"# ENCODER\n",
@@ -389,14 +284,15 @@
" activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n",
" tf.keras.layers.LeakyReLU(alpha=0.1),\n",
" \n",
" tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
"\n",
" tf.keras.layers.Dense(\n",
" units=12,\n",
" activation=tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
" activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n",
" \n",
" tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
"\n",
" tf.keras.layers.Dense(\n",
" units=8,\n",
" activation='linear', # Bottleneck stays linear\n",
@@ -408,20 +304,24 @@
"decoder = tf.keras.Sequential([\n",
" tf.keras.layers.Dense(\n",
" units=8,\n",
" activation=tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
" activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n",
" tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
"\n",
" tf.keras.layers.Dense(\n",
" units=12,\n",
" activation=tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
" activation=None,\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n",
" tf.keras.layers.LeakyReLU(negative_slope=0.1),\n",
"\n",
" tf.keras.layers.Dense(\n",
" units=X_train.shape[1],\n",
" activation='linear',\n",
" kernel_regularizer=tf.keras.regularizers.l2(reg)\n",
" ),\n",
"])\n"
"])"
]
},
{
@@ -456,7 +356,7 @@
"source": [
"history = autoencoder.fit(\n",
" X_train, X_train, # Input and target are the same for autoencoder\n",
" epochs=200,\n",
" epochs=50,\n",
" batch_size=64,\n",
" validation_data=(X_val_normal, X_val_normal),\n",
" verbose=1\n",
@@ -470,8 +370,7 @@
"metadata": {},
"outputs": [],
"source": [
"save_path = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/saved_models/encoder_model_2_neurons_minmax.keras\")\n",
"encoder.save(save_path)"
"encoder.save(enconder_path)"
]
},
{
@@ -489,8 +388,7 @@
"metadata": {},
"outputs": [],
"source": [
"load_path = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/saved_models/encoder_model_2_neurons_minmax.keras\")\n",
"encoder = tf.keras.models.load_model(load_path)"
"encoder = tf.keras.models.load_model(enconder_path)"
]
},
{
@@ -541,25 +439,6 @@
"test_predictions.shape"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "759118d8-989d-489c-9d35-331454b4795e",
"metadata": {},
"outputs": [],
"source": [
"all_zero = {\n",
" \"X_train_encoded\": np.all(X_train_encoded == 0),\n",
" \"X_val_normal_encoded\": np.all(X_val_normal_encoded == 0),\n",
" \"X_val_high_encoded\": np.all(X_val_high_encoded == 0),\n",
" \"X_test_normal_encoded\": np.all(X_test_normal_encoded == 0),\n",
" \"X_test_high_encoded\": np.all(X_test_high_encoded == 0),\n",
"}\n",
"\n",
"print(all_zero)\n",
"print(X_train_encoded.shape)"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -567,81 +446,49 @@
"metadata": {},
"outputs": [],
"source": [
"# fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n",
"fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n",
"\n",
"# # Subplot A: Normal\n",
"# axes[0].scatter(\n",
"# X_val_normal_encoded[:, 0],\n",
"# X_val_normal_encoded[:, 1],\n",
"# color=\"blue\",\n",
"# label=\"Normal\"\n",
"# )\n",
"# axes[0].set_title(\"Val Normal (encoded)\")\n",
"# axes[0].set_xlabel(\"latent feature 1\")\n",
"# axes[0].set_ylabel(\"latent feature 2\")\n",
"# axes[0].legend()\n",
"\n",
"# # Subplot B: High\n",
"# axes[1].scatter(\n",
"# X_val_high_encoded[:, 0],\n",
"# X_val_high_encoded[:, 1],\n",
"# color=\"orange\",\n",
"# label=\"High\"\n",
"# )\n",
"# axes[1].set_title(\"ValHigh (encoded)\")\n",
"# axes[1].set_xlabel(\"latent feature 1\")\n",
"# axes[1].set_ylabel(\"latent feature 2\")\n",
"# axes[1].legend()\n",
"\n",
"# # Subplot C: Both\n",
"# axes[2].scatter(\n",
"# X_val_normal_encoded[:, 0],\n",
"# X_val_normal_encoded[:, 1],\n",
"# color=\"blue\",\n",
"# label=\"Normal\"\n",
"# )\n",
"# axes[2].scatter(\n",
"# X_val_high_encoded[:, 0],\n",
"# X_val_high_encoded[:, 1],\n",
"# color=\"orange\",\n",
"# label=\"High\"\n",
"# )\n",
"# axes[2].set_title(\"Normal vs High (encoded)\")\n",
"# axes[2].set_xlabel(\"latent feature 1\")\n",
"# axes[2].set_ylabel(\"latent feature 2\")\n",
"# axes[2].legend()\n",
"\n",
"\n",
"\n",
"latent_dim = 8\n",
"fig, axes = plt.subplots(2, 4, figsize=(20, 10))\n",
"axes = axes.flatten() # flatten to index easily\n",
"\n",
"for i in range(latent_dim):\n",
" axes[i].scatter(\n",
" X_val_normal_encoded[:, i],\n",
" [0]*X_val_normal_encoded.shape[0], # optional: place on a line for 1D visualization\n",
" color='blue',\n",
" label='Normal',\n",
" alpha=0.6\n",
" )\n",
" axes[i].scatter(\n",
" X_val_high_encoded[:, i],\n",
" [0]*X_val_high_encoded.shape[0], # same for High\n",
" color='orange',\n",
" label='High',\n",
" alpha=0.6\n",
" )\n",
" axes[i].set_title(f'Latent dim {i+1}')\n",
" axes[i].set_xlabel(f'Feature {i+1}')\n",
" axes[i].set_yticks([]) # hide y-axis as it's just a 1D comparison\n",
" axes[i].legend()\n",
" axes[i].grid(True)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"# Subplot A: Normal\n",
"axes[0].scatter(\n",
" X_val_normal_encoded[:, 0],\n",
" X_val_normal_encoded[:, 1],\n",
" color=\"blue\",\n",
" label=\"Normal\"\n",
")\n",
"axes[0].set_title(\"Val Normal (encoded)\")\n",
"axes[0].set_xlabel(\"latent feature 1\")\n",
"axes[0].set_ylabel(\"latent feature 2\")\n",
"axes[0].legend()\n",
"\n",
"# Subplot B: High\n",
"axes[1].scatter(\n",
" X_val_high_encoded[:, 0],\n",
" X_val_high_encoded[:, 1],\n",
" color=\"orange\",\n",
" label=\"High\"\n",
")\n",
"axes[1].set_title(\"ValHigh (encoded)\")\n",
"axes[1].set_xlabel(\"latent feature 1\")\n",
"axes[1].set_ylabel(\"latent feature 2\")\n",
"axes[1].legend()\n",
"\n",
"# Subplot C: Both\n",
"axes[2].scatter(\n",
" X_val_normal_encoded[:, 0],\n",
" X_val_normal_encoded[:, 1],\n",
" color=\"blue\",\n",
" label=\"Normal\"\n",
")\n",
"axes[2].scatter(\n",
" X_val_high_encoded[:, 0],\n",
" X_val_high_encoded[:, 1],\n",
" color=\"orange\",\n",
" label=\"High\"\n",
")\n",
"axes[2].set_title(\"Normal vs High (encoded)\")\n",
"axes[2].set_xlabel(\"latent feature 1\")\n",
"axes[2].set_ylabel(\"latent feature 2\")\n",
"axes[2].legend()\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
@@ -708,11 +555,11 @@
"outputs": [],
"source": [
"# Save\n",
"with open('ocsvm_model.pkl', 'wb') as f:\n",
"with open(model_path, \"wb\") as f:\n",
" pickle.dump(ocsvm, f)\n",
"\n",
"# Load later\n",
"with open('ocsvm_model.pkl', 'rb') as f:\n",
"with open(model_path, \"rb\") as f:\n",
" ocsvm_loaded = pickle.load(f)"
]
},
@@ -731,11 +578,6 @@
"metadata": {},
"outputs": [],
"source": [
"# X_combined = np.concatenate([X_train_encoded, X_val_normal_encoded, X_val_high_encoded], axis=0)\n",
"# y_combined = np.concatenate([\n",
"# np.ones(X_train_encoded.shape[0]+X_val_normal_encoded.shape[0]), # normal = 1\n",
"# -np.ones(X_val_high_encoded.shape[0]) # anomaly = -1\n",
"# ], axis=0)\n",
"X_combined = np.concatenate([X_train_encoded, X_val_high_encoded], axis=0)\n",
"y_combined = np.concatenate([\n",
" np.ones(X_train_encoded.shape[0]), # normal = 1\n",
@@ -856,14 +698,6 @@
"source": [
"f1_score(y_true=np.concatenate([y_test_normal, y_test_high]), y_pred=predictions)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9e412041-7534-40fe-8486-ee97349a6168",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -871,18 +705,6 @@
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.10"
}
},
"nbformat": 4,
File diff suppressed because it is too large Load Diff
-877
View File
@@ -1,877 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "708c9745",
"metadata": {},
"source": [
"### Imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "53b10294",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"from pathlib import Path\n",
"import sys\n",
"import os\n",
"\n",
"base_dir = os.path.abspath(os.path.join(os.getcwd(), \"..\"))\n",
"sys.path.append(base_dir)\n",
"print(base_dir)\n",
"\n",
"from Fahrsimulator_MSY2526_AI.model_training.tools import evaluation_tools, scaler, mad_outlier_removal\n",
"from sklearn.preprocessing import StandardScaler, MinMaxScaler\n",
"from sklearn.svm import OneClassSVM\n",
"from sklearn.model_selection import GridSearchCV, KFold, ParameterGrid, train_test_split\n",
"import matplotlib.pyplot as plt\n",
"import tensorflow as tf\n",
"import pickle\n",
"from sklearn.metrics import (roc_auc_score, accuracy_score, precision_score, \n",
" recall_score, f1_score, confusion_matrix, classification_report) "
]
},
{
"cell_type": "markdown",
"id": "68101229",
"metadata": {},
"source": [
"### load Dataset"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "24a765e8",
"metadata": {},
"outputs": [],
"source": [
"dataset_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/first_AU_dataset/output_windowed.parquet\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "471001b0",
"metadata": {},
"outputs": [],
"source": [
"df = pd.read_parquet(path=dataset_path)"
]
},
{
"cell_type": "markdown",
"id": "0fdecdaa",
"metadata": {},
"source": [
"### Load Performance data and Subject Split"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "692d1b47",
"metadata": {},
"outputs": [],
"source": [
"performance_path = Path(r\"/home/jovyan/data-paulusjafahrsimulator-gpu/subject_performance/3new_au_performance.csv\")\n",
"performance_df = pd.read_csv(performance_path)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ea617e3f",
"metadata": {},
"outputs": [],
"source": [
"# Subject IDs aus dem Haupt-Dataset nehmen\n",
"subjects_from_df = df[\"subjectID\"].unique()\n",
"\n",
"# Performance-Subset nur für vorhandene Subjects\n",
"perf_filtered = performance_df[\n",
" performance_df[\"subjectID\"].isin(subjects_from_df)\n",
"][[\"subjectID\", \"overall_score\"]]\n",
"\n",
"# Merge: nur Subjects, die sowohl im df als auch im Performance-CSV vorkommen\n",
"merged = (\n",
" pd.DataFrame({\"subjectID\": subjects_from_df})\n",
" .merge(perf_filtered, on=\"subjectID\", how=\"inner\")\n",
")\n",
"\n",
"# Sicherstellen, dass keine Scores fehlen\n",
"if merged[\"overall_score\"].isna().any():\n",
" raise ValueError(\"Es fehlen Score-Werte für manche Subjects.\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ae43df8d",
"metadata": {},
"outputs": [],
"source": [
"merged_sorted = merged.sort_values(\"overall_score\", ascending=False).reset_index(drop=True)\n",
"\n",
"scores = merged_sorted[\"overall_score\"].values\n",
"n_total = len(merged_sorted)\n",
"n_small = n_total // 3\n",
"n_large = n_total - n_small\n",
"\n",
"# Schritt 1: zufällige Start-Aufteilung\n",
"idx = np.arange(n_total)\n",
"np.random.shuffle(idx)\n",
"\n",
"small_idx = idx[:n_small]\n",
"large_idx = idx[n_small:]\n",
"\n",
"def score_diff(small_idx, large_idx):\n",
" return abs(scores[small_idx].mean() - scores[large_idx].mean())\n",
"\n",
"diff = score_diff(small_idx, large_idx)\n",
"threshold = 0.01\n",
"max_iter = 100\n",
"count = 0\n",
"\n",
"# Schritt 2: random swaps bis Differenz klein genug\n",
"while diff > threshold and count < max_iter:\n",
" # Zwei zufällige Elemente auswählen\n",
" si = np.random.choice(small_idx)\n",
" li = np.random.choice(large_idx)\n",
" \n",
" # Tausch durchführen\n",
" new_small_idx = small_idx.copy()\n",
" new_large_idx = large_idx.copy()\n",
" \n",
" new_small_idx[new_small_idx == si] = li\n",
" new_large_idx[new_large_idx == li] = si\n",
"\n",
" # neue Differenz berechnen\n",
" new_diff = score_diff(new_small_idx, new_large_idx)\n",
"\n",
" # Swap akzeptieren, wenn es besser wird\n",
" if new_diff < diff:\n",
" small_idx = new_small_idx\n",
" large_idx = new_large_idx\n",
" diff = new_diff\n",
"\n",
" count += 1\n",
"\n",
"# Finalgruppen\n",
"group_small = merged_sorted.loc[small_idx].reset_index(drop=True)\n",
"group_large = merged_sorted.loc[large_idx].reset_index(drop=True)\n",
"\n",
"print(\"Finale Score-Differenz:\", diff)\n",
"print(\"Größe Gruppe 1:\", len(group_small))\n",
"print(\"Größe Gruppe 2:\", len(group_large))\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9d1b414e",
"metadata": {},
"outputs": [],
"source": [
"group_large['overall_score'].mean()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fa71f9a5",
"metadata": {},
"outputs": [],
"source": [
"group_small['overall_score'].mean()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79ecb4a2",
"metadata": {},
"outputs": [],
"source": [
"training_subjects = group_large['subjectID'].values\n",
"test_subjects = group_small['subjectID'].values\n",
"print(training_subjects)\n",
"print(test_subjects)"
]
},
{
"cell_type": "markdown",
"id": "4353f87c",
"metadata": {},
"source": [
"### Data cleaning with mad"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "76610052",
"metadata": {},
"outputs": [],
"source": [
"# SET\n",
"threshold_mad = 5\n",
"column_praefix ='AU'\n",
"\n",
"au_columns = [col for col in df.columns if col.startswith(column_praefix)]\n",
"cleaned_df = mad_outlier_removal.mad_outlier_removal(df,columns=au_columns, threshold=threshold_mad)\n",
"print(cleaned_df.shape)\n",
"print(df.shape)"
]
},
{
"cell_type": "markdown",
"id": "9a6c1732",
"metadata": {},
"source": [
"#### TO DO\n",
" * pipeline aus Autoencoder und SVM\n",
" * group k fold\n",
" * AE überpüfen, loss dokumentieren"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "877309d9",
"metadata": {},
"outputs": [],
"source": [
"### Variational Autoencoder with Classifier Head\n",
"import pandas as pd\n",
"import numpy as np\n",
"import tensorflow as tf\n",
"from tensorflow import keras\n",
"from tensorflow.keras import layers, Model\n",
"from sklearn.model_selection import GroupKFold\n",
"from sklearn.preprocessing import StandardScaler\n",
"from sklearn.metrics import (\n",
" accuracy_score, precision_score, recall_score, f1_score, \n",
" roc_auc_score, confusion_matrix, classification_report\n",
")\n",
"import matplotlib.pyplot as plt\n",
"from collections import defaultdict\n",
"\n",
"# ============================================================================\n",
"# 1. CREATE LABELS\n",
"# ============================================================================\n",
"\n",
"# Low workload: baseline + n-back level 1,4\n",
"low_all = cleaned_df[\n",
" ((cleaned_df[\"PHASE\"] == \"baseline\") |\n",
" ((cleaned_df[\"STUDY\"] == \"n-back\") & (cleaned_df[\"PHASE\"] != \"baseline\") & (cleaned_df[\"LEVEL\"].isin([1,4]))))\n",
"].copy()\n",
"low_all['label'] = 0\n",
"print(f\"Low workload samples: {low_all.shape[0]}\")\n",
"\n",
"# High workload n-back: level 2,3,5,6\n",
"high_nback = cleaned_df[\n",
" (cleaned_df[\"STUDY\"]==\"n-back\") &\n",
" (cleaned_df[\"LEVEL\"].isin([2, 3, 5, 6])) &\n",
" (cleaned_df[\"PHASE\"].isin([\"train\", \"test\"]))\n",
"].copy()\n",
"high_nback['label'] = 1\n",
"print(f\"High n-back samples: {high_nback.shape[0]}\")\n",
"\n",
"# High workload k-drive\n",
"high_kdrive = cleaned_df[\n",
" (cleaned_df[\"STUDY\"] == \"k-drive\") & (cleaned_df[\"PHASE\"] != \"baseline\")\n",
"].copy()\n",
"high_kdrive['label'] = 1\n",
"print(f\"High k-drive samples: {high_kdrive.shape[0]}\")\n",
"\n",
"# Combine all high workload\n",
"high_all = pd.concat([high_nback, high_kdrive])\n",
"print(f\"Total high workload samples: {high_all.shape[0]}\")\n",
"\n",
"# Complete labeled dataset\n",
"labeled_df = pd.concat([low_all, high_all]).reset_index(drop=True)\n",
"print(f\"\\nTotal labeled samples: {labeled_df.shape[0]}\")\n",
"print(f\"Class distribution:\\n{labeled_df['label'].value_counts()}\")\n",
"\n",
"# ============================================================================\n",
"# 2. TRAIN/TEST SPLIT BY SUBJECTS\n",
"# ============================================================================\n",
"\n",
"train_df = labeled_df[labeled_df['subjectID'].isin(training_subjects)].copy()\n",
"test_df = labeled_df[labeled_df['subjectID'].isin(test_subjects)].copy()\n",
"\n",
"print(f\"\\nTraining subjects: {training_subjects}\")\n",
"print(f\"Test subjects: {test_subjects}\")\n",
"print(f\"Train samples: {train_df.shape[0]}, Test samples: {test_df.shape[0]}\")\n",
"\n",
"# Extract features and labels\n",
"au_columns = [col for col in labeled_df.columns if col.startswith('AU')]\n",
"print(f\"\\nUsing {len(au_columns)} AU features: {au_columns}\")\n",
"\n",
"X_train = train_df[au_columns].values\n",
"y_train = train_df['label'].values\n",
"groups_train = train_df['subjectID'].values\n",
"\n",
"X_test = test_df[au_columns].values\n",
"y_test = test_df['label'].values\n",
"\n",
"# Normalize features\n",
"scaler = StandardScaler()\n",
"X_train_scaled = scaler.fit_transform(X_train)\n",
"X_test_scaled = scaler.transform(X_test)\n",
"\n",
"print(f\"\\nTrain class distribution: {np.bincount(y_train)}\")\n",
"print(f\"Test class distribution: {np.bincount(y_test)}\")\n",
"\n",
"# ============================================================================\n",
"# 3. VAE WITH CLASSIFIER HEAD MODEL\n",
"# ============================================================================\n",
"\n",
"class Sampling(layers.Layer):\n",
" \"\"\"Reparameterization trick for VAE\"\"\"\n",
" def call(self, inputs):\n",
" z_mean, z_log_var = inputs\n",
" batch = tf.shape(z_mean)[0]\n",
" dim = tf.shape(z_mean)[1]\n",
" epsilon = tf.random.normal(shape=(batch, dim))\n",
" return z_mean + tf.exp(0.5 * z_log_var) * epsilon\n",
"\n",
"def build_vae_classifier(input_dim, latent_dim, encoder_dims=[32, 16], \n",
" decoder_dims=[16, 32], classifier_dims=[16]):\n",
" \"\"\"\n",
" Build VAE with classifier head\n",
" \n",
" Args:\n",
" input_dim: Number of input features (20 AUs)\n",
" latent_dim: Dimension of latent space (2-5)\n",
" encoder_dims: Hidden layer sizes for encoder\n",
" decoder_dims: Hidden layer sizes for decoder\n",
" classifier_dims: Hidden layer sizes for classifier\n",
" \"\"\"\n",
" \n",
" # ---- ENCODER ----\n",
" encoder_inputs = keras.Input(shape=(input_dim,), name='encoder_input')\n",
" x = encoder_inputs\n",
" \n",
" for i, dim in enumerate(encoder_dims):\n",
" x = layers.Dense(dim, activation='relu', name=f'encoder_dense_{i}')(x)\n",
" x = layers.BatchNormalization(name=f'encoder_bn_{i}')(x)\n",
" x = layers.Dropout(0.2, name=f'encoder_dropout_{i}')(x)\n",
" \n",
" z_mean = layers.Dense(latent_dim, name='z_mean')(x)\n",
" z_log_var = layers.Dense(latent_dim, name='z_log_var')(x)\n",
" z = Sampling()([z_mean, z_log_var])\n",
" \n",
" encoder = Model(encoder_inputs, [z_mean, z_log_var, z], name='encoder')\n",
" \n",
" # ---- DECODER ----\n",
" latent_inputs = keras.Input(shape=(latent_dim,), name='latent_input')\n",
" x = latent_inputs\n",
" \n",
" for i, dim in enumerate(decoder_dims):\n",
" x = layers.Dense(dim, activation='relu', name=f'decoder_dense_{i}')(x)\n",
" x = layers.BatchNormalization(name=f'decoder_bn_{i}')(x)\n",
" \n",
" decoder_outputs = layers.Dense(input_dim, activation='linear', name='decoder_output')(x)\n",
" decoder = Model(latent_inputs, decoder_outputs, name='decoder')\n",
" \n",
" # ---- CLASSIFIER HEAD ----\n",
" x = latent_inputs\n",
" for i, dim in enumerate(classifier_dims):\n",
" x = layers.Dense(dim, activation='relu', name=f'classifier_dense_{i}')(x)\n",
" x = layers.Dropout(0.3, name=f'classifier_dropout_{i}')(x)\n",
" \n",
" classifier_output = layers.Dense(1, activation='sigmoid', name='classifier_output')(x)\n",
" classifier = Model(latent_inputs, classifier_output, name='classifier')\n",
" \n",
" # ---- FULL MODEL ----\n",
" inputs = keras.Input(shape=(input_dim,), name='vae_input')\n",
" z_mean, z_log_var, z = encoder(inputs)\n",
" reconstructed = decoder(z)\n",
" classification = classifier(z)\n",
" \n",
" model = Model(inputs, [reconstructed, classification], name='vae_classifier')\n",
" \n",
" return model, encoder, decoder, classifier\n",
"\n",
"# ============================================================================\n",
"# 4. CUSTOM TRAINING LOOP WITH COMBINED LOSS\n",
"# ============================================================================\n",
"\n",
"class VAEClassifier(keras.Model):\n",
" def __init__(self, encoder, decoder, classifier, **kwargs):\n",
" super().__init__(**kwargs)\n",
" self.encoder = encoder\n",
" self.decoder = decoder\n",
" self.classifier = classifier\n",
" self.total_loss_tracker = keras.metrics.Mean(name=\"total_loss\")\n",
" self.reconstruction_loss_tracker = keras.metrics.Mean(name=\"reconstruction_loss\")\n",
" self.kl_loss_tracker = keras.metrics.Mean(name=\"kl_loss\")\n",
" self.classification_loss_tracker = keras.metrics.Mean(name=\"classification_loss\")\n",
" self.accuracy_tracker = keras.metrics.BinaryAccuracy(name=\"accuracy\")\n",
" \n",
" @property\n",
" def metrics(self):\n",
" return [\n",
" self.total_loss_tracker,\n",
" self.reconstruction_loss_tracker,\n",
" self.kl_loss_tracker,\n",
" self.classification_loss_tracker,\n",
" self.accuracy_tracker,\n",
" ]\n",
" \n",
" def train_step(self, data):\n",
" x, y = data\n",
" \n",
" with tf.GradientTape() as tape:\n",
" # Forward pass\n",
" z_mean, z_log_var, z = self.encoder(x, training=True)\n",
" reconstruction = self.decoder(z, training=True)\n",
" classification = self.classifier(z, training=True)\n",
" \n",
" # Reconstruction loss (MSE)\n",
" reconstruction_loss = tf.reduce_mean(\n",
" keras.losses.mse(x, reconstruction))\n",
" \n",
" # KL divergence loss\n",
" kl_loss = -0.5 * tf.reduce_mean(\n",
" tf.reduce_sum(\n",
" 1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var),\n",
" axis=1\n",
" )\n",
" )\n",
" \n",
" # Classification loss (binary crossentropy)\n",
" # Classification loss (binary crossentropy)\n",
" classification_loss = tf.reduce_mean(\n",
" keras.losses.binary_crossentropy(tf.expand_dims(y, -1), classification)\n",
" )\n",
" \n",
" # Combined loss with weights\n",
" total_loss = reconstruction_loss + kl_loss + classification_loss\n",
" \n",
" # Backpropagation\n",
" grads = tape.gradient(total_loss, self.trainable_weights)\n",
" self.optimizer.apply_gradients(zip(grads, self.trainable_weights))\n",
" \n",
" # Update metrics\n",
" self.total_loss_tracker.update_state(total_loss)\n",
" self.reconstruction_loss_tracker.update_state(reconstruction_loss)\n",
" self.kl_loss_tracker.update_state(kl_loss)\n",
" self.classification_loss_tracker.update_state(classification_loss)\n",
" self.accuracy_tracker.update_state(y, classification)\n",
" \n",
" return {\n",
" \"total_loss\": self.total_loss_tracker.result(),\n",
" \"reconstruction_loss\": self.reconstruction_loss_tracker.result(),\n",
" \"kl_loss\": self.kl_loss_tracker.result(),\n",
" \"classification_loss\": self.classification_loss_tracker.result(),\n",
" \"accuracy\": self.accuracy_tracker.result(),\n",
" }\n",
" \n",
" def test_step(self, data):\n",
" x, y = data\n",
" \n",
" z_mean, z_log_var, z = self.encoder(x, training=False)\n",
" reconstruction = self.decoder(z, training=False)\n",
" classification = self.classifier(z, training=False)\n",
" \n",
" # Reconstruction loss (MSE)\n",
" reconstruction_loss = tf.reduce_mean(\n",
" keras.losses.mse(x, reconstruction))\n",
" kl_loss = -0.5 * tf.reduce_mean(\n",
" tf.reduce_sum(1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var), axis=1)\n",
" )\n",
" # Classification loss (binary crossentropy)\n",
" classification_loss = tf.reduce_mean(\n",
" keras.losses.binary_crossentropy(tf.expand_dims(y, -1), classification)\n",
" )\n",
" total_loss = reconstruction_loss + kl_loss + classification_loss\n",
" \n",
" self.total_loss_tracker.update_state(total_loss)\n",
" self.reconstruction_loss_tracker.update_state(reconstruction_loss)\n",
" self.kl_loss_tracker.update_state(kl_loss)\n",
" self.classification_loss_tracker.update_state(classification_loss)\n",
" self.accuracy_tracker.update_state(y, classification)\n",
" \n",
" return {\n",
" \"total_loss\": self.total_loss_tracker.result(),\n",
" \"reconstruction_loss\": self.reconstruction_loss_tracker.result(),\n",
" \"kl_loss\": self.kl_loss_tracker.result(),\n",
" \"classification_loss\": self.classification_loss_tracker.result(),\n",
" \"accuracy\": self.accuracy_tracker.result(),\n",
" }\n",
"\n",
"# ============================================================================\n",
"# 5. GROUP K-FOLD CROSS-VALIDATION WITH GRID SEARCH\n",
"# ============================================================================\n",
"\n",
"# Hyperparameter grid\n",
"param_grid = {\n",
" 'latent_dim': [2, 5],\n",
" 'encoder_dims': [[32, 16], [64, 32]],\n",
" 'learning_rate': [0.001, 0.005],\n",
" 'batch_size': [32, 64],\n",
"}\n",
"\n",
"# Generate all combinations\n",
"from itertools import product\n",
"keys = param_grid.keys()\n",
"values = param_grid.values()\n",
"param_combinations = [dict(zip(keys, v)) for v in product(*values)]\n",
"\n",
"print(f\"\\nTotal hyperparameter combinations: {len(param_combinations)}\")\n",
"\n",
"# Group K-Fold setup\n",
"n_splits = 5\n",
"gkf = GroupKFold(n_splits=n_splits)\n",
"\n",
"# Store results\n",
"cv_results = []\n",
"\n",
"# Grid search with cross-validation\n",
"for idx, params in enumerate(param_combinations):\n",
" print(f\"\\n{'='*80}\")\n",
" print(f\"Testing combination {idx+1}/{len(param_combinations)}: {params}\")\n",
" print(f\"{'='*80}\")\n",
" \n",
" fold_results = []\n",
" \n",
" for fold, (train_idx, val_idx) in enumerate(gkf.split(X_train_scaled, y_train, groups_train)):\n",
" print(f\"\\nFold {fold+1}/{n_splits}\")\n",
" \n",
" X_fold_train, X_fold_val = X_train_scaled[train_idx], X_train_scaled[val_idx]\n",
" y_fold_train, y_fold_val = y_train[train_idx], y_train[val_idx]\n",
" \n",
" # Build model\n",
" model, encoder, decoder, classifier = build_vae_classifier(\n",
" input_dim=len(au_columns),\n",
" latent_dim=params['latent_dim'],\n",
" encoder_dims=params['encoder_dims'],\n",
" decoder_dims=list(reversed(params['encoder_dims'])),\n",
" classifier_dims=[16]\n",
" )\n",
" \n",
" vae_classifier = VAEClassifier(encoder, decoder, classifier)\n",
" vae_classifier.compile(optimizer=keras.optimizers.Adam(params['learning_rate']))\n",
" \n",
" # Early stopping\n",
" early_stop = keras.callbacks.EarlyStopping(\n",
" monitor='val_total_loss',\n",
" patience=10,\n",
" restore_best_weights=True,\n",
" mode='min'\n",
" )\n",
" \n",
" # Train\n",
" history = vae_classifier.fit(\n",
" X_fold_train, y_fold_train,\n",
" validation_data=(X_fold_val, y_fold_val),\n",
" epochs=60,\n",
" batch_size=params['batch_size'],\n",
" callbacks=[early_stop],\n",
" verbose=0\n",
" )\n",
" \n",
" # Evaluate on validation fold\n",
" z_mean_val, _, _ = encoder.predict(X_fold_val, verbose=0)\n",
" y_pred_proba = classifier.predict(z_mean_val, verbose=0).flatten()\n",
" y_pred = (y_pred_proba > 0.5).astype(int)\n",
" \n",
" fold_metrics = {\n",
" 'accuracy': accuracy_score(y_fold_val, y_pred),\n",
" 'precision': precision_score(y_fold_val, y_pred, zero_division=0),\n",
" 'recall': recall_score(y_fold_val, y_pred, zero_division=0),\n",
" 'f1': f1_score(y_fold_val, y_pred, zero_division=0),\n",
" 'roc_auc': roc_auc_score(y_fold_val, y_pred_proba),\n",
" 'final_recon_loss': history.history['val_reconstruction_loss'][-1],\n",
" 'final_kl_loss': history.history['val_kl_loss'][-1],\n",
" 'final_class_loss': history.history['val_classification_loss'][-1],\n",
" }\n",
" \n",
" fold_results.append(fold_metrics)\n",
" print(f\" Accuracy: {fold_metrics['accuracy']:.4f}, F1: {fold_metrics['f1']:.4f}, AUC: {fold_metrics['roc_auc']:.4f}\")\n",
" \n",
" # Clear session to free memory\n",
" keras.backend.clear_session()\n",
" \n",
" # Average across folds\n",
" avg_results = {\n",
" 'params': params,\n",
" 'mean_accuracy': np.mean([r['accuracy'] for r in fold_results]),\n",
" 'std_accuracy': np.std([r['accuracy'] for r in fold_results]),\n",
" 'mean_f1': np.mean([r['f1'] for r in fold_results]),\n",
" 'std_f1': np.std([r['f1'] for r in fold_results]),\n",
" 'mean_roc_auc': np.mean([r['roc_auc'] for r in fold_results]),\n",
" 'std_roc_auc': np.std([r['roc_auc'] for r in fold_results]),\n",
" 'mean_recon_loss': np.mean([r['final_recon_loss'] for r in fold_results]),\n",
" 'mean_kl_loss': np.mean([r['final_kl_loss'] for r in fold_results]),\n",
" 'mean_class_loss': np.mean([r['final_class_loss'] for r in fold_results]),\n",
" 'fold_results': fold_results\n",
" }\n",
" \n",
" cv_results.append(avg_results)\n",
" \n",
" print(f\"\\nMean CV Accuracy: {avg_results['mean_accuracy']:.4f} ± {avg_results['std_accuracy']:.4f}\")\n",
" print(f\"Mean CV F1: {avg_results['mean_f1']:.4f} ± {avg_results['std_f1']:.4f}\")\n",
" print(f\"Mean CV AUC: {avg_results['mean_roc_auc']:.4f} ± {avg_results['std_roc_auc']:.4f}\")\n",
"\n",
"# ============================================================================\n",
"# 6. SELECT BEST MODEL AND EVALUATE ON TEST SET\n",
"# ============================================================================\n",
"\n",
"# Find best hyperparameters based on mean F1 score\n",
"best_idx = np.argmax([r['mean_f1'] for r in cv_results])\n",
"best_params = cv_results[best_idx]['params']\n",
"\n",
"print(f\"\\n{'='*80}\")\n",
"print(\"BEST HYPERPARAMETERS (based on CV F1 score):\")\n",
"print(f\"{'='*80}\")\n",
"for key, value in best_params.items():\n",
" print(f\"{key}: {value}\")\n",
"print(f\"\\nCV Performance:\")\n",
"print(f\" Accuracy: {cv_results[best_idx]['mean_accuracy']:.4f} ± {cv_results[best_idx]['std_accuracy']:.4f}\")\n",
"print(f\" F1 Score: {cv_results[best_idx]['mean_f1']:.4f} ± {cv_results[best_idx]['std_f1']:.4f}\")\n",
"print(f\" ROC-AUC: {cv_results[best_idx]['mean_roc_auc']:.4f} ± {cv_results[best_idx]['std_roc_auc']:.4f}\")\n",
"\n",
"# Train final model on all training data\n",
"print(f\"\\n{'='*80}\")\n",
"print(\"TRAINING FINAL MODEL ON ALL TRAINING DATA\")\n",
"print(f\"{'='*80}\")\n",
"\n",
"final_model, final_encoder, final_decoder, final_classifier = build_vae_classifier(\n",
" input_dim=len(au_columns),\n",
" latent_dim=best_params['latent_dim'],\n",
" encoder_dims=best_params['encoder_dims'],\n",
" decoder_dims=list(reversed(best_params['encoder_dims'])),\n",
" classifier_dims=[16]\n",
")\n",
"\n",
"final_vae_classifier = VAEClassifier(final_encoder, final_decoder, final_classifier)\n",
"final_vae_classifier.compile(optimizer=keras.optimizers.Adam(best_params['learning_rate']))\n",
"\n",
"final_history = final_vae_classifier.fit(\n",
" X_train_scaled, y_train,\n",
" validation_split=0.2,\n",
" epochs=100,\n",
" batch_size=best_params['batch_size'],\n",
" callbacks=[keras.callbacks.EarlyStopping(monitor='val_total_loss', patience=15, restore_best_weights=True, mode='min')],\n",
" verbose=1\n",
")\n",
"\n",
"# Evaluate on held-out test set\n",
"print(f\"\\n{'='*80}\")\n",
"print(\"EVALUATION ON HELD-OUT TEST SET\")\n",
"print(f\"{'='*80}\")\n",
"\n",
"z_mean_test, _, _ = final_encoder.predict(X_test_scaled, verbose=0)\n",
"y_test_pred_proba = final_classifier.predict(z_mean_test, verbose=0).flatten()\n",
"y_test_pred = (y_test_pred_proba > 0.5).astype(int)\n",
"\n",
"test_metrics = {\n",
" 'accuracy': accuracy_score(y_test, y_test_pred),\n",
" 'precision': precision_score(y_test, y_test_pred),\n",
" 'recall': recall_score(y_test, y_test_pred),\n",
" 'f1': f1_score(y_test, y_test_pred),\n",
" 'roc_auc': roc_auc_score(y_test, y_test_pred_proba),\n",
"}\n",
"\n",
"print(\"\\nTest Set Performance:\")\n",
"for metric, value in test_metrics.items():\n",
" print(f\" {metric.capitalize()}: {value:.4f}\")\n",
"\n",
"print(\"\\nConfusion Matrix:\")\n",
"print(confusion_matrix(y_test, y_test_pred))\n",
"\n",
"print(\"\\nClassification Report:\")\n",
"print(classification_report(y_test, y_test_pred, target_names=['Low Workload', 'High Workload']))\n",
"\n",
"# ============================================================================\n",
"# 7. VISUALIZATION\n",
"# ============================================================================\n",
"\n",
"# Plot training history\n",
"fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n",
"\n",
"axes[0, 0].plot(final_history.history['reconstruction_loss'], label='Train')\n",
"axes[0, 0].plot(final_history.history['val_reconstruction_loss'], label='Val')\n",
"axes[0, 0].set_title('Reconstruction Loss')\n",
"axes[0, 0].set_xlabel('Epoch')\n",
"axes[0, 0].set_ylabel('Loss')\n",
"axes[0, 0].legend()\n",
"axes[0, 0].grid(True)\n",
"\n",
"axes[0, 1].plot(final_history.history['kl_loss'], label='Train')\n",
"axes[0, 1].plot(final_history.history['val_kl_loss'], label='Val')\n",
"axes[0, 1].set_title('KL Divergence Loss')\n",
"axes[0, 1].set_xlabel('Epoch')\n",
"axes[0, 1].set_ylabel('Loss')\n",
"axes[0, 1].legend()\n",
"axes[0, 1].grid(True)\n",
"\n",
"axes[1, 0].plot(final_history.history['classification_loss'], label='Train')\n",
"axes[1, 0].plot(final_history.history['val_classification_loss'], label='Val')\n",
"axes[1, 0].set_title('Classification Loss')\n",
"axes[1, 0].set_xlabel('Epoch')\n",
"axes[1, 0].set_ylabel('Loss')\n",
"axes[1, 0].legend()\n",
"axes[1, 0].grid(True)\n",
"\n",
"axes[1, 1].plot(final_history.history['accuracy'], label='Train')\n",
"axes[1, 1].plot(final_history.history['val_accuracy'], label='Val')\n",
"axes[1, 1].set_title('Classification Accuracy')\n",
"axes[1, 1].set_xlabel('Epoch')\n",
"axes[1, 1].set_ylabel('Accuracy')\n",
"axes[1, 1].legend()\n",
"axes[1, 1].grid(True)\n",
"\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Visualize latent space (if 2D or 3D)\n",
"if best_params['latent_dim'] == 2:\n",
" z_mean_train, _, _ = final_encoder.predict(X_train_scaled, verbose=0)\n",
" \n",
" plt.figure(figsize=(10, 8))\n",
" scatter = plt.scatter(z_mean_train[:, 0], z_mean_train[:, 1], \n",
" c=y_train, cmap='RdYlBu', alpha=0.6, edgecolors='k')\n",
" plt.colorbar(scatter, label='Workload (0=Low, 1=High)')\n",
" plt.xlabel('Latent Dimension 1')\n",
" plt.ylabel('Latent Dimension 2')\n",
" plt.title('2D Latent Space Representation (Training Data)')\n",
" plt.grid(True, alpha=0.3)\n",
" plt.show()\n",
" \n",
" # Test set latent space\n",
" plt.figure(figsize=(10, 8))\n",
" scatter = plt.scatter(z_mean_test[:, 0], z_mean_test[:, 1], \n",
" c=y_test, cmap='RdYlBu', alpha=0.6, edgecolors='k')\n",
" plt.colorbar(scatter, label='Workload (0=Low, 1=High)')\n",
" plt.xlabel('Latent Dimension 1')\n",
" plt.ylabel('Latent Dimension 2')\n",
" plt.title('2D Latent Space Representation (Test Data)')\n",
" plt.grid(True, alpha=0.3)\n",
" plt.show()\n",
"\n",
"print(\"\\n\" + \"=\"*80)\n",
"print(\"TRAINING COMPLETE!\")\n",
"print(\"=\"*80)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79bcfc58",
"metadata": {},
"outputs": [],
"source": [
"### Save Trained VAE Classifier Model\n",
"from pathlib import Path\n",
"from datetime import datetime\n",
"\n",
"# Define save path\n",
"model_dir = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/trained_models\")\n",
"model_dir.mkdir(parents=True, exist_ok=True)\n",
"\n",
"timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
"model_path = model_dir / f\"vae_classifier_{timestamp}.keras\"\n",
"\n",
"# Save the complete model\n",
"final_vae_classifier.save(model_path)\n",
"\n",
"print(f\"Model saved to: {model_path}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d700e517",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "30d8d100",
"metadata": {},
"outputs": [],
"source": [
"### Plot Confusion Matrix for Final Model\n",
"from sklearn.metrics import ConfusionMatrixDisplay\n",
"x = Path(\"/home/jovyan/data-paulusjafahrsimulator-gpu/trained_models/vae_classifier_20251210_230121.keras\")\n",
"# Load the saved model\n",
"print(f\"Loading model from: {x}\")\n",
"# loaded_vae_classifier = tf.keras.models.load_model(x)\n",
"loaded_vae_classifier = final_vae_classifier\n",
"print(\"✓ Model loaded successfully!\")\n",
"\n",
"# Extract encoder and classifier from loaded model\n",
"loaded_encoder = loaded_vae_classifier.encoder\n",
"loaded_classifier = loaded_vae_classifier.classifier\n",
"\n",
"# Get predictions on test set\n",
"z_mean_test, _, _ = loaded_encoder.predict(X_test_scaled, verbose=0)\n",
"y_test_pred_proba = loaded_classifier.predict(z_mean_test, verbose=0).flatten()\n",
"y_test_pred = (y_test_pred_proba > 0.5).astype(int)\n",
"\n",
"# Create and plot confusion matrix\n",
"cm = confusion_matrix(y_test, y_test_pred)\n",
"disp = ConfusionMatrixDisplay(confusion_matrix=cm, \n",
" display_labels=['Low Workload', 'High Workload'])\n",
"\n",
"fig, ax = plt.subplots(figsize=(8, 6))\n",
"disp.plot(ax=ax, cmap='Blues', values_format='d')\n",
"plt.title('Confusion Matrix - Test Set (Loaded Model)')\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"# Print metrics\n",
"print(f\"\\nTest Set Performance (Loaded Model):\")\n",
"print(f\" Accuracy: {accuracy_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" Precision: {precision_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" Recall: {recall_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" F1 Score: {f1_score(y_test, y_test_pred):.4f}\")\n",
"print(f\" ROC-AUC: {roc_auc_score(y_test, y_test_pred_proba):.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "e826a998",
"metadata": {},
"source": [
"TO DO\n",
" * autoencoder langsam anfangen mit 19 schichten\n",
" * dann AE und SVM mit hybridem training wie bei claude?!\n",
" * dataset aus eyetracking verwenden?"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.10"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -21,3 +21,42 @@ def mad_outlier_removal(df, columns, threshold=3.5, c=1.4826):
final_mask = np.logical_and.reduce(masks)
return df_clean[final_mask]
def calculate_mad_params(df, columns):
"""
Calculate median and MAD parameters for each column.
This should be run ONLY on the training data.
Returns a dictionary: {col: (median, mad)}
"""
params = {}
for col in columns:
median = df[col].median()
mad = np.median(np.abs(df[col] - median))
params[col] = (median, mad)
return params
def apply_mad_filter(df, params, threshold=3.5):
"""
Apply MAD-based outlier removal using precomputed parameters.
Works on training, validation, and test data.
df: DataFrame to filter
params: dictionary {col: (median, mad)} from training data
threshold: cutoff for robust Z-score
"""
df_clean = df.copy()
for col, (median, mad) in params.items():
if mad == 0:
continue # no spread; nothing to remove for this column
robust_z = 0.6745 * (df_clean[col] - median) / mad
outlier_mask = np.abs(robust_z) > threshold
# Remove values only in this specific column
df_clean.loc[outlier_mask, col] = median
print(df_clean.shape)
return df_clean
-11
View File
@@ -1,11 +0,0 @@
# from tools import db_helpers
import sys
def main():
print(sys.version)
# db_helpers.add_columns_to_table()
if __name__ == "__main__":
main()
+6 -6
View File
@@ -1,20 +1,20 @@
database:
path: "C:\\repo\\Fahrsimulator_MSY2526_AI\\predict_pipeline\\database.sqlite"
path: "/home/edgekit/MSY_FS/databases/database.sqlite"
table: feature_table
key: _Id
model:
path: "C:\\repo\\Fahrsimulator_MSY2526_AI\\files_for_testing\\xgb_model_3_groupK.joblib"
path: "/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/cnn_crossVal_EarlyFusion_V2_0103.keras"
scaler:
use_scaling: True
path: "C:\\repo\\Fahrsimulator_MSY2526_AI\\predict_pipeline\\normalizer_min_max_global.pkl"
use_scaling: true
path: "/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/scaler_crossVal_EarlyFusion_V2_0103.joblib"
mqtt:
enabled: true
host: "141.75.215.233"
host: "141.75.223.13"
port: 1883
topic: "PREDICTIONS"
topic: "PREDICTION"
client_id: "jetson-board"
qos: 0
retain: false
-9
View File
@@ -1,9 +0,0 @@
import sqlite3
def main():
return 0
if __name__ == "__main__":
main()
+50 -8
View File
@@ -1,5 +1,13 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "fb68b447",
"metadata": {},
"source": [
"## Database creation and filling (for live system) "
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -21,8 +29,10 @@
"metadata": {},
"outputs": [],
"source": [
"database_path = Path(r\"/home/edgekit/MSY_FS/databases/rawdata.sqlite\")\n",
"parquet_path = Path(r\"/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/files_for_testing/both_mod_0000.parquet\")"
"# TODO: set paths and table name\n",
"database_path = Path(r\"database.sqlite\") # this path references an empty, but already created sqlite file\n",
"parquet_path = Path(r\"...parquet\") # this path leads to the data that should be used to fill the databse\n",
"table_name = \"XXX\" # name of the new table"
]
},
{
@@ -56,6 +66,14 @@
"con, cursor = db_helpers.connect_db(database_path)"
]
},
{
"cell_type": "markdown",
"id": "7007c68f",
"metadata": {},
"source": [
"Select a subset to insert into database "
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -69,6 +87,14 @@
"df_first_100.insert(0, '_Id', df_first_100.index + 1)"
]
},
{
"cell_type": "markdown",
"id": "92171186",
"metadata": {},
"source": [
"Type conversion"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -88,6 +114,14 @@
" return \"TEXT\"\n"
]
},
{
"cell_type": "markdown",
"id": "45af9956",
"metadata": {},
"source": [
"Define constraints and primary key"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -109,6 +143,14 @@
"}\n"
]
},
{
"cell_type": "markdown",
"id": "133e92ee",
"metadata": {},
"source": [
"Create the table"
]
},
{
"cell_type": "code",
"execution_count": null,
@@ -119,7 +161,7 @@
"sql = db_helpers.create_table(\n",
" conn=con,\n",
" cursor=cursor,\n",
" table_name=\"rawdata\",\n",
" table_name=table_name,\n",
" columns=columns,\n",
" constraints=constraints,\n",
" primary_key=primary_key,\n",
@@ -150,7 +192,7 @@
"db_helpers.insert_rows_into_table(\n",
" conn=con,\n",
" cursor=cursor,\n",
" table_name=\"rawdata\",\n",
" table_name=table_name,\n",
" columns=columns_to_insert,\n",
" commit=True\n",
")\n"
@@ -163,7 +205,7 @@
"metadata": {},
"outputs": [],
"source": [
"a = db_helpers.get_data_from_table(conn=con, table_name='rawdata',columns_list=['*'])"
"request = db_helpers.get_data_from_table(conn=con, table_name='rawdata',columns_list=['*'])"
]
},
{
@@ -173,7 +215,7 @@
"metadata": {},
"outputs": [],
"source": [
"a.head()"
"request.head()"
]
},
{
@@ -189,7 +231,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "MSY_FS_env",
"display_name": "310",
"language": "python",
"name": "python3"
},
@@ -203,7 +245,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
"version": "3.10.19"
}
},
"nbformat": 4,
+2 -2
View File
@@ -5,7 +5,7 @@ StartLimitIntervalSec=0
[Service]
Type=oneshot
User=edgekit
ExecStart=~/anaconda3/envs/p310_FS/bin/python /home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/predict_sample.py
ExecStart=/home/edgekit/anaconda3/envs/p310_FS_TF/bin/python /home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/predict_sample.py
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target
+2 -1
View File
@@ -2,10 +2,11 @@
Description=Run predict sample every 5 seconds
[Timer]
OnBootSec=5
OnActiveSec=60
OnUnitActiveSec=5
AccuracySec=1s
Unit=predict.service
[Install]
WantedBy=timers.target
+28 -34
View File
@@ -7,9 +7,9 @@ import sys
import yaml
import pickle
sys.path.append('/home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/tools')
# sys.path.append(r"c:\\repo\\Fahrsimulator_MSY2526_AI\\tools")
import db_helpers
import joblib
import paho.mqtt.client as mqtt
def _load_serialized(path: Path):
suffix = path.suffix.lower()
@@ -52,11 +52,12 @@ def callModel(sample, model_path):
suffix = model_path.suffix.lower()
if suffix in {".pkl", ".joblib"}:
model = _load_serialized(model_path)
# elif suffix == ".keras":
# import tensorflow as tf
# model = tf.keras.models.load_model(model_path)
# else:
# raise ValueError(f"Unsupported model format: {suffix}. Use .pkl, .joblib, or .keras.")
elif suffix == ".keras":
import tensorflow
tensorflow.get_logger().setLevel("ERROR")
model = tensorflow.keras.models.load_model(model_path)
else:
raise ValueError(f"Unsupported model format: {suffix}. Use .pkl, .joblib, or .keras.")
x = np.asarray(sample, dtype=np.float32)
if x.ndim == 1:
@@ -64,9 +65,7 @@ def callModel(sample, model_path):
if suffix == ".keras":
x_full = x
# Future model (35 features): keep this call when your new model is active.
# prediction = model.predict(x_full[:, :35], verbose=0)
prediction = model.predict(x_full[:, :20], verbose=0)
prediction = (model.predict(x_full[:, :35], verbose=0) > 0.5).astype(int)
else:
if hasattr(model, "predict"):
@@ -127,42 +126,37 @@ def sendMessage(config_file_path, message):
payload = json.dumps(message, ensure_ascii=False)
print(payload)
# Later: publish via MQTT using config parameters above.
# Example (kept commented intentionally):
# import paho.mqtt.client as mqtt
# client = mqtt.Client(client_id=mqtt_cfg.get("client_id", "predictor-01"))
# if "username" in mqtt_cfg and mqtt_cfg.get("username"):
# client.username_pw_set(mqtt_cfg["username"], mqtt_cfg.get("password"))
# client.connect(mqtt_cfg.get("host", "localhost"), int(mqtt_cfg.get("port", 1883)), 60)
# client.publish(
# topic=topic,
# payload=payload,
# qos=int(mqtt_cfg.get("qos", 1)),
# retain=bool(mqtt_cfg.get("retain", False)),
# )
# client.disconnect()
# publish via MQTT using config parameters above.
client = mqtt.Client(client_id=mqtt_cfg.get("client_id", "predictor-01"),
callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
callback_api_version=mqtt.CallbackAPIVersion.VERSION2
if "username" in mqtt_cfg and mqtt_cfg.get("username"):
client.username_pw_set(mqtt_cfg["username"], mqtt_cfg.get("password"))
client.connect(mqtt_cfg.get("host", "localhost"), int(mqtt_cfg.get("port", 1883)), 60)
client.publish(
topic=topic,
payload=payload,
qos=int(mqtt_cfg.get("qos", 1)),
retain=bool(mqtt_cfg.get("retain", False)),
)
client.disconnect()
return
def replace_nan(sample, config_file_path: Path):
with config_file_path.open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f)
fallback_list = cfg.get("fallback", [])
fallback_map = {}
for item in fallback_list:
if isinstance(item, dict):
fallback_map.update(item)
fallback_map = cfg.get("fallback", {})
if sample.empty:
return False, sample
nan_ratio = sample.isna().mean()
valid = nan_ratio <= 0.5
if valid and fallback_map:
sample = sample.fillna(value=fallback_map)
return valid, sample
def sample_to_numpy(sample, drop_cols=("_Id", "start_time")):
@@ -0,0 +1,216 @@
# Predict Service and Timer Documentation
## Overview
This setup uses **systemd services and timers** to repeatedly execute a
Python script that performs prediction on the latest sample and sends a
message.
The systemd unit files are typically stored in:
/etc/systemd/system/
For this setup, the relevant files are:
/etc/systemd/system/predict.service
/etc/systemd/system/predict.timer
These files define the service execution and the timer scheduling.
- `predict.service` -- defines how the Python script is executed
- `predict.timer` -- schedules the repeated execution of the service
The timer triggers the service **every 5 seconds** after the first
activation.
------------------------------------------------------------------------
# Systemd Timer
File: `predict.timer`
``` ini
[Unit]
Description=Run predict sample every 5 seconds
[Timer]
OnActiveSec=60
OnUnitActiveSec=5
AccuracySec=1s
Unit=predict.service
[Install]
WantedBy=timers.target
```
## Behavior
- **OnActiveSec=60**\
The timer starts **60 seconds after it is activated**.
- **OnUnitActiveSec=5**\
After the service has run once, it will be triggered again **every 5
seconds**.
- **AccuracySec=1s**\
Allows systemd to schedule the timer with **1 second precision**.
- **Unit=predict.service**\
Defines which service should be triggered by the timer.
------------------------------------------------------------------------
# Systemd Service
File: `predict.service`
``` ini
[Unit]
Description=Predict latest sample and send message
After=network.target
StartLimitIntervalSec=0
[Service]
Type=oneshot
User=edgekit
ExecStart=/home/edgekit/anaconda3/envs/p310_FS_TF/bin/python /home/edgekit/MSY_FS/fahrsimulator_msy2526_ai/predict_pipeline/predict_sample.py
[Install]
WantedBy=multi-user.target
```
## Behavior
- **Type=oneshot**\
The service runs the script once and then exits.
- **User=edgekit**\
The script is executed under the `edgekit` user.
- **ExecStart**\
Executes the Python script using the specified conda environment.
- **After=network.target**\
Ensures the service only runs after the network is available.
------------------------------------------------------------------------
# Execution Flow
1. The **timer starts** after it is enabled.
2. After **60 seconds**, the first execution happens (this results from the duration of the camera processing initialization)
3. The timer triggers `predict.service`.
4. The service runs `predict_sample.py`.
5. Once the script finishes, the service exits.
6. The timer triggers the service again **every 5 seconds**.
------------------------------------------------------------------------
# Debugging and Monitoring
## View Live Output
All `print()` output from the Python script is written to the **systemd
journal**.
Follow the output live with:
``` bash
journalctl -u predict.service -f
```
This command is typically the most useful for debugging.
------------------------------------------------------------------------
# Common Systemd Commands
## Check Service Status
``` bash
systemctl status predict.service
```
Shows the last execution result and recent log lines.
------------------------------------------------------------------------
## Check Timer Status
``` bash
systemctl status predict.timer
```
Shows when the timer last ran and when it will run next.
------------------------------------------------------------------------
## List All Timers
``` bash
systemctl list-timers
```
Displays all active timers and their next scheduled execution.
------------------------------------------------------------------------
# Manual Execution
To run the service manually once:
``` bash
systemctl start predict.service
```
------------------------------------------------------------------------
# Restarting the Systemd Units
## Restart the Service
``` bash
systemctl restart predict.service
```
## Restart the Timer
``` bash
systemctl restart predict.timer
```
------------------------------------------------------------------------
## Reload Systemd After Changes
If `.service` or `.timer` files were modified:
``` bash
systemctl daemon-reload
systemctl restart predict.timer
systemctl restart predict.service
```
------------------------------------------------------------------------
## Enabling the Timer
To ensure the timer starts automatically on system boot:
``` bash
systemctl enable predict.timer
systemctl start predict.timer
```
------------------------------------------------------------------------
# Summary
- `predict.timer` schedules periodic execution.
- `predict.service` runs the Python prediction script.
- The script runs **every 5 seconds** after the initial delay.
- Logs and script output are available through:
``` bash
journalctl -u predict.service -f
```
+196
View File
@@ -0,0 +1,196 @@
name: 'prediction_env'
channels:
- defaults
- conda-forge
dependencies:
- _py-xgboost-mutex=2.0=cpu_2
- absl-py=2.3.1=py310haa95532_0
- aom=3.12.1=h00a0c3c_0
- arrow-cpp=21.0.0=hcdc3a1c_2
- asttokens=3.0.1=pyhd8ed1ab_0
- astunparse=1.6.3=py_0
- aws-c-auth=0.9.0=h02ab6af_2
- aws-c-cal=0.9.2=h02ab6af_1
- aws-c-common=0.12.4=h02ab6af_0
- aws-c-compression=0.3.1=h02ab6af_2
- aws-c-event-stream=0.5.6=h02ab6af_0
- aws-c-http=0.10.4=h02ab6af_0
- aws-c-io=0.21.4=h02ab6af_0
- aws-c-mqtt=0.13.3=h02ab6af_0
- aws-c-s3=0.8.7=h02ab6af_0
- aws-c-sdkutils=0.2.4=h02ab6af_1
- aws-checksums=0.2.7=h02ab6af_1
- aws-crt-cpp=0.34.0=h885b0b7_0
- aws-sdk-cpp=1.11.638=hf0af688_0
- blas=1.0=mkl
- brotlicffi=1.2.0.0=py310h885b0b7_0
- bzip2=1.0.8=h2bbff1b_6
- c-ares=1.34.6=h2c209ce_0
- ca-certificates=2026.1.4=h4c7d964_0
- cairo=1.18.4=he9e932c_0
- certifi=2026.01.04=py310haa95532_0
- cffi=2.0.0=py310h02ab6af_1
- charset-normalizer=3.4.4=py310haa95532_0
- colorama=0.4.6=pyhd8ed1ab_1
- comm=0.2.3=pyhe01879c_0
- dav1d=1.2.1=h2bbff1b_0
- debugpy=1.8.20=py310h699e580_0
- decorator=5.2.1=pyhd8ed1ab_0
- exceptiongroup=1.3.1=pyhd8ed1ab_0
- executing=2.2.1=pyhd8ed1ab_0
- expat=2.7.4=hd7fb8db_0
- flatbuffers=24.3.25=h21716d4_0
- fontconfig=2.15.0=hd211d86_0
- freeglut=3.8.0=hfcef157_0
- freetype=2.14.1=hfbffc0b_0
- fribidi=1.0.16=haf45083_0
- gast=0.7.0=pyhd3eb1b0_0
- gflags=2.2.2=hd77b12b_1
- giflib=5.2.2=h7edc060_0
- glog=0.5.0=hd77b12b_1
- google-pasta=0.2.0=pyhd3eb1b0_0
- graphite2=1.3.14=hd77b12b_1
- grpcio=1.74.1=py310h5c751cc_0
- h5py=3.15.1=py310he283ef2_1
- harfbuzz=12.3.0=h3ef6528_1
- hdf5=1.14.5=ha36df97_2
- icc_rt=2022.1.0=h6049295_2
- icu=73.1=h6c2663c_0
- idna=3.11=py310haa95532_0
- intel-openmp=2025.0.0=haa95532_1164
- ipykernel=7.2.0=pyh6dadd2b_1
- ipython=8.37.0=pyha7b4d00_0
- jedi=0.19.2=pyhd8ed1ab_1
- joblib=1.5.3=py310haa95532_0
- jpeg=9f=ha349fce_0
- jupyter_client=8.8.0=pyhcf101f3_0
- jupyter_core=5.9.1=pyh6dadd2b_0
- keras=3.11.2=py310h51baaa3_0
- krb5=1.21.3=hdf4eb48_0
- lcms2=2.17=h3732fa5_0
- lerc=4.0.0=h5da7b33_0
- libabseil=20250814.1=cxx17_hcd311fc_0
- libavif=1.3.0=h5bd13ec_0
- libbrotlicommon=1.2.0=h907acca_0
- libbrotlidec=1.2.0=h02c67a5_0
- libbrotlienc=1.2.0=h483e6b9_0
- libcurl=8.17.0=h6e672f4_1
- libdeflate=1.22=h5bf469e_0
- libexpat=2.7.4=hd7fb8db_0
- libffi=3.4.4=hd77b12b_1
- libglib=2.86.3=h9bccc14_0
- libgrpc=1.74.1=hde67744_0
- libhwloc=2.12.1=default_hfa10c62_1000
- libiconv=1.16=h2bbff1b_3
- libkrb5=1.22.1=hb237eb7_0
- libopenjpeg=2.5.4=h02ab6af_1
- libpng=1.6.54=ha15c746_0
- libprotobuf=6.33.0=h2a56892_1
- libre2-11=2025.11.05=ha6b10e7_0
- libsodium=1.0.20=hc70643c_0
- libssh2=1.11.1=h2addb87_0
- libthrift=0.22.0=ha2884a9_0
- libtiff=4.7.1=h3a18249_0
- libwebp-base=1.6.0=hbf3958f_0
- libxgboost=3.1.2=h585ebfc_0
- libxml2=2.13.9=h6201b9f_0
- libzlib=1.3.1=h02ab6af_0
- lz4-c=1.9.4=h2bbff1b_1
- m2w64-gcc-libgfortran=5.3.0=6
- m2w64-gcc-libs=5.3.0=7
- m2w64-gcc-libs-core=5.3.0=7
- m2w64-gmp=6.1.0=2
- m2w64-libwinpthread-git=5.0.0.4634.697f757=2
- markdown=3.10=py310haa95532_0
- markdown-it-py=2.2.0=py310haa95532_1
- markupsafe=3.0.2=py310h827c3e9_0
- matplotlib-inline=0.2.1=pyhd8ed1ab_0
- mdurl=0.1.2=py310haa95532_0
- mkl=2025.0.0=h5da7b33_930
- mkl-service=2.5.2=py310h0b37514_0
- mkl_fft=2.1.1=py310h300f80d_0
- mkl_random=1.3.0=py310ha5e6156_0
- ml_dtypes=0.5.4=py310h42c1672_0
- mpi=1.0=msmpi
- mpi4py=4.0.3=py310h02ab6af_1
- msmpi=10.1.1=had4844c_0
- msys2-conda-epoch=20160418=1
- namex=0.1.0=py310haa95532_0
- nest-asyncio=1.6.0=pyhd8ed1ab_1
- numpy-base=2.1.3=py310he4e2855_3
- openssl=3.6.1=hf411b9b_1
- opt_einsum=3.3.0=pyhd3eb1b0_1
- optree=0.18.0=py310h03f52e7_0
- orc=2.2.0=h79e1e1e_1
- packaging=25.0=py310haa95532_1
- paho-mqtt=2.1.0=pyhe01879c_1
- parso=0.8.6=pyhcf101f3_0
- pcre2=10.46=h5740b90_0
- pickleshare=0.7.5=pyhd8ed1ab_1004
- pillow=12.1.0=py310h6b7a805_0
- pip=26.0.1=pyhc872135_0
- pixman=0.46.4=h4043f72_0
- platformdirs=4.9.2=pyhcf101f3_0
- prompt-toolkit=3.0.52=pyha770c72_0
- protobuf=6.33.0=py310ha4c6e68_0
- psutil=7.2.2=py310h1637853_0
- pure_eval=0.2.3=pyhd8ed1ab_1
- py-xgboost=3.1.2=py310haa95532_0
- pyarrow=21.0.0=py310h42c1672_1
- pycparser=2.23=py310haa95532_0
- pygments=2.19.2=pyhd8ed1ab_0
- pysocks=1.7.1=py310haa95532_1
- python=3.10.19=h981015d_0
- python-dateutil=2.9.0.post0=pyhe01879c_2
- python-flatbuffers=24.3.25=py310haa95532_0
- python_abi=3.10=2_cp310
- pywin32=311=py310h282bd7d_1
- pyyaml=6.0.3=py310hb9a58be_0
- pyzmq=27.1.0=py310h535538e_0
- re2=2025.11.05=hc24cdf5_0
- requests=2.32.5=py310haa95532_1
- rich=14.2.0=py310haa95532_0
- scipy=1.15.3=py310h1bbe36f_1
- setuptools=80.10.2=py310haa95532_0
- six=1.17.0=pyhe01879c_1
- snappy=1.2.2=hab6b7b3_1
- sqlite=3.51.1=hda9a48d_0
- stack_data=0.6.3=pyhd8ed1ab_1
- tbb=2022.3.0=h90c84d6_0
- tbb-devel=2022.3.0=h90c84d6_0
- tensorboard=2.20.0=py310haa95532_0
- tensorboard-data-server=0.7.0=py310haa95532_1
- tensorflow=2.20.0=cpu_py310h6605a60_0
- tensorflow-base=2.20.0=cpu_py310hce87ebc_0
- termcolor=3.2.0=py310haa95532_0
- threadpoolctl=3.5.0=py310h4442805_1
- tk=8.6.15=hf199647_0
- tornado=6.5.4=py310h29418f3_0
- traitlets=5.14.3=pyhd8ed1ab_1
- typing-extensions=4.15.0=py310haa95532_0
- typing_extensions=4.15.0=py310haa95532_0
- ucrt=10.0.22621.0=haa95532_0
- urllib3=2.6.3=py310haa95532_0
- utf8proc=2.6.1=h2bbff1b_1
- vc=14.42=haa95532_5
- vc14_runtime=14.44.35208=h4927774_10
- vs2015_runtime=14.44.35208=ha6b5a95_10
- wcwidth=0.6.0=pyhd8ed1ab_0
- werkzeug=3.1.3=py310haa95532_0
- wheel=0.46.3=py310haa95532_0
- win_inet_pton=1.1.0=py310haa95532_1
- wrapt=2.0.1=py310h02ab6af_0
- xgboost=3.1.2=py310haa95532_0
- xz=5.6.4=h4754444_1
- yaml=0.2.5=he774522_0
- zeromq=4.3.5=h5bddc39_9
- zlib=1.3.1=h02ab6af_0
- zstd=1.5.7=h56299aa_0
- pip:
- numpy==1.24.4
- pandas==2.3.0
- pyocclient==0.6
- pytz==2025.2
- scikit-learn==1.6.1
- tzdata==2025.3
+551
View File
@@ -0,0 +1,551 @@
# Project Report: Multimodal Driver State Analysis
## 1) Project Scope
This repository implements an end-to-end workflow for multimodal driver-state analysis in a simulator setup.
The system combines:
- Facial Action Units (AUs)
- Eye-tracking features (fixations, saccades, blinks, pupil behavior)
Apart from this, several machine learning model architectures are presented and evaluated.
Content:
- Dataset generation
- Exploratory data analysis
- Model training experiments
- Real-time inference with SQlite, systemd and MQTT
- Repository file inventory
- Additional nformation
## 2) Dataset generation
### 2.1 Data Access, Filtering, and Data Conversion
Main scripts:
- `dataset_creation/create_parquet_files_from_owncloud.py`
- `dataset_creation/parquet_file_creation.py`
Purpose:
- Download and/or access dataset files (either download first via ```EDA/owncloud_file_access.ipynb``` or all in one with ```dataset_creation/create_parquet_files_from_owncloud.py```
- Keep relevant columns (FACE_AUs and eye-tracking raw values)
- Filter invalid samples (e.g., invalid level segments): Make sure not to drop rows where NaN is necessary for later feature creation, therefore use subset argument in dropNa()!
- Export subject-level parquet files
- Before running the scripts: be aware that the whole dataset contains 30 files with around 900 Mbytes each, provide enough storage and expect this to take a while.
### 2.2 Feature Engineering (Offline)
Main script:
- `dataset_creation/combined_feature_creation.py`
Behavior:
- Builds fixed-size sliding windows over subject time series (window size and step size can be adjusted)
- Uses prepared parquet files from 2.1
- Aggregates AU statistics per window (e.g., `FACE_AUxx_mean`)
- Computes eye-feature aggregates (fix/sacc/blink/pupil metrics)
- Produces training-ready feature tables = dataset
- Parameter ```MIN_DUR_BLINKS``` can be adjusted, although this value needs to make sense in combination with your sampling frequency
- With low videostream rates, consider to reevaluate the meaningfulness of some eye-tracking features, especially the fixations
- running the script requires a manual installation of [pygaze Analyser library](https://github.com/esdalmaijer/PyGazeAnalyser.git) from github
### 2.3 Online Camera + Eye + AU Feature Extraction
Main scripts:
- `dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py`
- `dataset_creation/camera_handling/eyeFeature_new.py`
- `dataset_creation/camera_handling/db_helper.py`
Runtime behavior:
- Captures webcam stream with OpenCV
- Extracts gaze/iris-based signals via MediaPipe
- Records overlapping windows (`VIDEO_DURATION=50s`, `START_INTERVAL=5s`, `FPS=25`)
- Runs AU extraction (`py-feat`) from recorded video segments
- Explanation of the py-feat functionality is located in `dataset_creation/AU_creation/pyfeat_docu.ipynb`
- Computes eye-feature summary from generated gaze parquet
- Writes merged rows to SQLite table `feature_table`
Operational note:
- `DB_PATH` and other paths are currently code-configured and must be adapted per deployment.
### 2.4 Two Approaches to Eye-Tracking Data Collection
Eye-tracking can be implemented using two main approaches:
Used Approach: Relative Iris Position
- Tracks the position of the pupil within the eye region
- The position is normalized relative to the eye itself
- No reference to the screen or physical environment is required
Not Used Approach: Screen Calibration
- Requires the user to look at 9 predefined points on the screen
- A mapping model is trained based on these points
- Establishes a relationship between eye movement and screen coordinates
Important Considerations (for both methods)
- Keep the head as still as possible
- Ensure consistent and even lighting conditions
## 3) EDA
The directory EDA provides several files to get insights into both the raw data from AdaBase and your own dataset.
- `EDA.ipynb` - Main EDA notebook: recreates the plot from AdaBase documentation, lists all experiments and in general serves as a playground for you to get to know the files.
- `distribution_plots.ipynb` - This notebook aimes to visualize the data distributions for each experiment - the goal is the find out, whether the split of experiments into high and low cognitive load is clearer if some experiments are dropped.
- `histogramms.ipynb` - Histogram analysis of low load vs high load per feature. Additionaly, scatter plots per feature are available.
- `researchOnSubjectPerformance.ipynb` - This noteboooks aims to see how the performance values range for the 30 subjects. The code creates and saves a table in csv-format, which will later be used as the foundation of the performance based split in ```model_training/tools/performance_based_split```
- `owncloud_file_access.ipynb` - Get access to the files via owncloud and safe them as .h5 files, in correspondence to the parquet file creation script
- `login.yaml` -Used to store URL and password to access files from owncloud, used in previous notebook
- `calculate_replacement_values.ipynb` -Fallback / median computation notebook for deployment, creation of yaml syntax embedding
General information:
- Due to their size, its absolutely recommended to download and save the dataset files once in the beginning
- For better data understanding, read the [AdaBase publication](https://www.mdpi.com/1424-8220/23/1/340)
## 4) Model Training
Included model families:
- CNN variants (different fusion strategies)
- XGBoost
- Isolation Forest*
- OCSVM*
- DeepSVDD*
\* These training strategies are unsupervised, which means only low cognitive load samples are used for training. Validation then also considers high low samples.
Supporting utilities in ```model_training/tools```:
- `scaler.py`: Functions to fit, transform, save and load either MinMaxScaler or StandardScaler, subject-wise and globally - for new subjects, a fallback scaler (using mean of all subjects scaling parameters) is used
- `performance_split.py`: Provides a function to split a group of subjects based on their performance in the AdaBase experiments, based on the results created in `researchOnSubjectPerformance.ipynb`. To split into three groups for train, validation & test, call the function twice
- `mad_outlier_removal.py`: Functions to fit and transform data with MAD outlier removal
- `evaluation_tools.py`: Especially used for Isolation Forest, Functions for ROC curve as well as confusion matrix
### 4.1 CNNs
This section summarizes all CNN‑based supervised learning approaches implemented in the project.
All models operate on facial Action Unit (AU) features and, depending on the notebook, additional eye‑tracking features.
The notebooks differ in evaluation methodology, fusion strategy, and experimental intention.
### 4.1.1 Baseline CNN (Notebook: *CNN_simple*)
The first notebook implements a simple 1D CNN to establish a baseline for AU‑only classification.
The model uses two convolutional layers, batch normalization, max pooling, and a regularized dense head.
A single subject‑exclusive train/validation/test split is used.
The intention behind this notebook is to:
- Provide a baseline performance level
- Validate that AU features contain discriminative information
- Identify overfitting tendencies before moving to more rigorous evaluation
### 4.1.2 Cross‑Validated CNN (Notebook: *CNN_crossVal*)
This notebook introduces 5‑fold GroupKFold cross‑validation, ensuring subject‑exclusive folds.
The architecture is similar to the baseline but includes stronger regularization and a lower learning rate.
The intention behind this notebook is to:
- Provide robust generalization estimates
- Reduce variance caused by single‑split evaluation
- Establish a cross‑validated AU‑only benchmark
### 4.1.3 Cross‑Validated CNN (Face AUs Only) (Notebook: *CNN_crossVal_faceAUs*)
This notebook is a streamlined version of the previous one, removing unused eye‑tracking features and focusing exclusively on AUs.
The intention behind this notebook is to:
- Provide a clean AU‑only benchmark
- Improve reproducibility and interpretability
- Prepare for multimodal comparisons
### 4.1.4 Cross‑Validated CNN with Early Fusion (AUs + Eye Features)
(Notebook: *CNN_crossVal_faceAUs_eyeFeatures*)
This notebook introduces early fusion, concatenating AU and eye‑tracking features into a single input vector.
The architecture remains identical to AU‑only models.
The intention behind this notebook is to:
- Evaluate whether multimodal early fusion improves performance
- Establish a first multimodal baseline
- Analyze class‑specific behavior via confusion matrices
This notebook didn't lead to any useful results.
### 4.1.5 Cross‑Validated CNN with Early Fusion (Refined Version) (Notebook: *CNN_crossVal_EarlyFusion*)
This notebook refines the early‑fusion approach by removing samples with missing values and ensuring consistent multimodal input quality.
The intention behind this notebook is to:
- Provide a clean and fully validated early‑fusion model
- Investigate multimodal complementarity under rigorous CV
- Improve interpretability through aggregated confusion matrices
### 4.1.6 Cross‑Validated CNN with Early Fusion and Subset Filtering (Notebook: *CNN_crossVal_EarlyFusion_Filter*)
This notebook applies domain‑specific filtering to isolate a more homogeneous subset of cognitive states before training.
The intention behind this notebook is to:
- Evaluate whether subset filtering improves multimodal learning
- Reduce dataset heterogeneity
- Provide a controlled multimodal benchmark
### 4.1.7 Hybrid‑Fusion CNN (Notebook: *CNN_crossVal_HybridFusion*)
This notebook introduces a hybrid‑fusion architecture with two modality‑specific branches:
- A 1D CNN for AUs
- A dense MLP for eye‑tracking features
The branches are fused before classification.
The intention behind this notebook is to:
- Allow each modality to learn specialized representations
- Evaluate whether hybrid fusion outperforms early fusion
- Provide a strong multimodal benchmark
### 4.1.8 Early‑Fusion CNN with Independent Test Evaluation (Notebook: *CNN_crossVal_EarlyFusion_Test_Eval*)
This notebook introduces the first true held‑out test evaluation for an early‑fusion CNN.
A subject‑exclusive train/test split is created before cross‑validation.
The intention behind this notebook is to:
- Provide a deployment‑realistic performance estimate
- Compare validation‑fold behavior with true test‑set behavior
- Visualize ROC and PR curves for threshold analysis
| Metric / Model | CNN_crossVal_EarlyFusion_Test_Eval |
|----------------|-------------------------------------|
| Test Accuracy | 0.913 |
| Test F1 | 0.927 |
| Test AUC | 0.967 |
| Balanced Accuracy | 0.907 |
| Precision | 0.918 |
| Recall | 0.937 |
#### Confusion Matrix
![Konfusionsmatrix](results/Konfusionsmatrix_EarlyFusion.png)
*Figure 4.1.8.1: Confusion matrix of the Early‑Fusion model.*
#### ROC-Curve
![ROC-Kurve](results/ROC_EarlyFusion.png)
*Figure 4.1.8.2: ROC-Curve of the Early‑Fusion model.*
### 4.1.9 Hybrid‑Fusion CNN with Independent Test Evaluation (Notebook: *CNN_crossVal_HybridFusion_Test_Eval*)
This notebook extends hybrid fusion with a subject‑exclusive train/test split and full test‑set evaluation.
The intention behind this notebook is to:
- Evaluate hybrid fusion under realistic deployment conditions
- Compare hybrid vs. early fusion on unseen subjects
- Provide full diagnostic plots (ROC, PR, confusion matrices)
| Metric / Model | CNN_crossVal_HybridFusion_Test_Eval |
|----------------|--------------------------------------|
| Test Accuracy | 0.950 |
| Test F1 | 0.959 |
| Test AUC | 0.983 |
| Balanced Accuracy | 0.942 |
| Precision | 0.933 |
| Recall | 0.986 |
#### Confusion Matrix
![Konfusionsmatrix](results/Konfusionsmatrix_HybridFusion.png)
*Figure 4.1.9.1: Confusion matrix of the Hybrid‑Fusion model.*
#### ROC-Curve
![ROC-Kurve](results/ROC_HybridFusion.png)
*Figure 4.1.9.2: ROC-Curve of the Hybrid‑Fusion model.*
### 4.1.10 Summary
Across all nine notebooks, the project progresses from a simple AU‑only baseline to advanced multimodal hybrid‑fusion architectures with independent test evaluation.
The final experiments revealed that hybrid fusion provides a measurable performance advantage over early fusion. While both approaches achieve strong results, the hybrid‑fusion model reaches higher overall accuracy (95% vs. 91.3%) and substantially stronger recall (98.6% vs. 93.7%), indicating that it is more effective at correctly identifying high‑workload samples.
Early fusion, however, shows slightly better precision, suggesting that it produces fewer false positives.
Looking ahead, further improvements could likely be achieved through more extensive hyperparameter tuning, as the current results suggest that additional optimization headroom remains.
### 4.2 XGBoost
This documentation outlines the evolution of the XGBoost classification pipeline for cognitive workload detection. The project transitioned from a basic unimodal setup to a sophisticated, multi-stage hybrid system incorporating advanced statistical filtering and deep feature extraction.
During the model creation several methods were used to improve the model accuracy. During training, the biggest challenge was always the high overfitting of the model. Even in the last version with explicit regulation parameters the overall accuracy couldn't be improved more than the different methods before.
The model overall was not that good, as the highest accuracy we could achieve was around 65%, which is a bit higher than Fraunhofer achieved in the ADABase-Paper.
### 4.2.1 Classical XGBoost Baseline
To establish a performance baseline, a classical Extreme Gradient Boosting (XGBoost) model was implemented. XGBoost was selected for its ability to handle non-linear relationships and its inherent regularization, which helps prevent overfitting in high-dimensional feature spaces like Facial Action Units. XGBoost was picked because of its usage in the ADABase Paper. Initially, the model utilized raw Action Unit sums with global normalization to determine the basic predictability of workload from facial muscle activity alone.
| Metric / Model | Classical XGBoost |
| --- | --- |
| Accuracy | 0.581 |
| AUC | 0.562 |
| F1-Score | 0.652 |
### 4.2.2 XGBoost with GroupKFold Validation
To address the challenge of inter-subject variability, the validation strategy was upgraded to `GroupKFold`. In behavioral data, samples from the same subject are highly correlated. Standard cross-validation often leads to data leakage, where the model memorizes individual facial characteristics. By ensuring that a subject's data is never shared between the training and validation sets, this iteration provides a scientifically rigorous measure of how the model generalizes to entirely unseen individuals.
| Metric / Model | XGBoost (GroupKFold) |
| --- | --- |
| Accuracy | 0.586 |
| AUC | 0.573 |
| F1-Score | 0.651 |
### 4.2.3 Hybrid XGBoost with Autoencoder
To improve feature quality, a hybrid approach was introduced by pre-training a deep Autoencoder. The encoder branch was used to compress 20 raw Action Units into a 5-dimensional latent space. This non-linear dimensionality reduction aims to capture muscle synergies and filter out noise that decision trees might struggle with. The XGBoost classifier was then trained on these machine-learned representations rather than raw inputs.
| Metric / Model | XGBoost + Autoencoder |
| --- | --- |
| Accuracy | 0.589 |
| AUC | 0.575 |
| F1-Score | 0.650 |
### 4.2.4 Robust XGBoost with MAD Outlier Removal
Recognizing that physiological and AU data often contain sensor artifacts, a robust preprocessing layer was added using Median Absolute Deviation (MAD). Unlike standard deviation, MAD is resilient to extreme outliers. By calculating a Robust Z-score and filtering signals in the training set, the model learned from a "clean" representation of cognitive states, significantly improving the stability of the gradient boosting process.
| Metric / Model | XGBoost + MAD |
| --- | --- |
| Accuracy | 0.641 |
| AUC | 0.610 |
| F1-Score | 0.733 |
### 4.2.5 Combined Dataset of Action Units and EyeTracking
This iteration involved training, which refined a robust pipeline on a new, expanded dataset. This dataset integrated both high-frequency facial action units and advanced eye-tracking metrics (pupillometry and fixations).
Since the recreation of the EyeTracking data in the lab was in doubt, only Action Units were used in the first XGBoost models. Now the model also implemented the EyeTracking data as features.
By applying performance-based subject splitting, we ensured that the training and test sets were balanced not only by label but by the subjects' underlying skill levels, resulting in the most deployable version of the AI.
| Metric / Model | Final Combined Model |
| --- | --- |
| Accuracy | 0.659 |
| AUC | 0.621 |
| F1-Score | 0.715 |
### 4.2.6 Regularized XGBoost with Complexity Control
Building upon the robust preprocessing of the previous steps, this iteration focuses on strict **complexity control** within the XGBoost architecture. To mitigate the 100% training accuracy observed in earlier unimodal tests—a clear indicator of overfitting—we introduced explicit **L1 (reg_alpha)** and **L2 (reg_lambda)** regularization parameters into the GridSearch space.
By penalizing large weights and promoting feature sparsity, the model is forced to prioritize the most globally relevant Action Units. Furthermore, the tree depth was intentionally restricted (`max_depth`: 2-4), and an **Early Stopping** callback with a 30-round patience window was implemented. This ensures that training terminates at the point of optimal generalization, capturing the essential physiological trends of cognitive load while ignoring subject-specific noise.
| Metric / Model | Regularized XGBoost |
| --- | --- |
| Accuracy | 0.665 |
| AUC | 0.646 |
| F1-Score | 0.727 |
### 4.3 Isolation Forest
To start with unsupervised learning techniques, `IsolationForest.ipynb`was created to research how well a simple ensemble classificator performs on the created dataset.
The notebook comes with one class grid search for hyperparameter tuning as well as a ROC curve that allows manual fine tuning.
Overall, our experiments have shown, that this approach is not sufficient, with the following results:
| Metric / Model | Isolation Forest |
|----------------|---------|
| Best Balanced Accuracy |0.57|
| Best AUC | 0.61|
In detail, the classificator tends to classify the majority of samples as low load and is therefore not sufficient to be used for later deployment.
### 4.4 One Class SVM with Autoencoder
The training of an On Class SVM on the data from the dataset resulted in every sample was predicted as an anomaly.
In the next step, an autoencoder is pretrained to learn representation of the data. Afterwards, the encoder is used for preprocessing, which leads to OCSVM training on encoder output.
The training includes hyperparameter tuning through gridsearch cv.
Encoder output is visualized with print statements and plots that show the encoded data for both low and high load samples.
We see that the encoder struggles to represent the unseen high load samples differently. As a consequence, the One Class SVM also does not achieve sufficient performance.
| Metric / Model | One Class SVM |
|----------------|---------|
| Best Balanced Accuracy |0.62|
When the notebook is run completely, both the trained encoder and svm are saved for later use given the save paths are set correctly.
### 4.5 Deep SVDD
Similar to the OCSVM training, an autoencoder is used to preprocess the data before the actual Deep SVDD training. Nevertheless, the usage is partialy different. The Dee SVDD uses a pretrained encoder to fine tune it by apllying a different loss function (which results from the theoretical concept behind Deep SVDD). This means that the encoder weights are still modified in the actual Deep SVDD training.
Also, this approach includes **hybrid fusion of modalities**. Instead of putting all features into the same input layer, the neural network is divided into two branches, that process action units and eye-tracking features separately.
Then, after two Dense layers each, the branches are fusioned by concatenation. From there, another two Dense layers process the data.
The decoder is not exactly similar, as the split of the modalities happens are the very end.
To compute the total loss, loss from both modalities is combined by sum. Users are able to change loss weights. Training includes 2x2 phases, both autoencoder and later Deep SVDD are first trained with larger learning rate, then fine tuned with a smaller learning rate.
| Metric / Model | Deep SVDD |
|----------------|---------|
| Best Balanced Accuracy |0.60|
| Best AUC | 0.57|
### 4.6 General information on unsupervised approaches
As described above, the approachs didn't meet the requirements in terms of prediction performance. For all models, both MinMax-Scaling as well as Standard-Scaling was done. Also, both subjectwise and globally. Unfortunately, the differences were not that large, which may explain why preprocessing wasn't mentioned above.
Future research should always keep in mind while subject-wise scaling might be better for training, it makes deployment on new subjects more difficult. Our solution, as implemented in `model_training/tools/scaler.py` calculates a fallback scaler (using mean of all subjects scaling parameters).
## 5) Real-Time Prediction and Messaging
Main script:
- `predict_pipeline/predict_sample.py`
Pipeline:
- Loads runtime config (`predict_pipeline/config.yaml`)
- Pulls latest row from SQLite
- Replaces missing values using `fallback` map from config file - if more than 50% of values need to be replaced, the sample is dropped and "valid=False"
- Optionally applies scaler (`.pkl`/`.joblib`) - set via config file
- Loads model (`.keras`, `.pkl`, `.joblib`) and predicts
- Publishes JSON payload to MQTT topic
Expected payload form:
```json
{
"valid": true, # false only if too many signals are invalid
"_id": 123, # this is the sample ID from the database
"prediction": 0 # 0 for low load, 1 for high load
}
```
### 5.1 Scheduled Prediction (Linux)
Files:
- `predict_pipeline/predict.service`
- `predict_pipeline/predict.timer`
Role:
- Run inference repeatedly without manual execution
- Timer/service configuration can be customized
More information on how to use and interact with the system service and timer can be found in [predict_service_timer_documentation.md](/predict_pipeline/predict_service_timer_documentation.md)
## 5.2 Runtime Configuration
Primary config file:
- `predict_pipeline/config.yaml`
Sections:
- `database`: SQLite location + table + sort key
- `model`: model path
- `scaler`: scaler usage + path
- `mqtt`: broker and publish format
- `sample.columns`: expected feature order
- `fallback`: default values for NaN replacement
Important:
- The repository currently uses environment-specific absolute paths in some scripts/configs to ensure functionality on Ohm-UX driving simulator.
## 5.3 Data and Feature Expectations
Prediction expects SQLite rows containing:
- `_Id`
- `start_time` - this is not yet used for either predictions or messages
- All configured model features (AUs + eye metrics)
Common feature groups (similar to own dataset):
- `FACE_AUxx_mean` columns
- Fixation counters and duration statistics
- Saccade count/amplitude/duration statistics
- Blink count/duration statistics
- Pupil mean and IPA
## 5.4 Create database from scratch
To (re-)create the custom database for deployment, use `fill_db.ipynb`. Enter the path to your dataset, drop unnecessary columns and insert a subset of data with tool functions from `tools/db_helpers`
## 6) Installation and Dependencies
Due to unsolvable dependency conflicts, several environemnts need to be used in the same time.
### 6.1 Environemnt for camera handling
The setup of a virtual environment for the camera handling is difficult due to vary dependency conflicts.
Therefore it is necessary to create the virtual environment with every package in the specific version and each package in the specific order.
Furthermore the environment needs to be based on Python 3.10. The specific versions and order of the packages are described int the file:
`requirements.txt`
### 6.2 Environment for predictions
If you want to use the existing deployment on Ohm-UX driving simulator's jetson board, activate conda environment `p310_FS_TF`, a python 3.10 environment including tensorflow and all other packages required to run `predict_sample.py`
Otherwise, as described in `readme.md: Setup`, you can use `prediction_env.yaml`, to create a new environment that fulfills the requirements.
## 7) Repository File Inventory
### Root
- `.gitignore` - Git ignore rules
- `readme.md` - minimal quickstart documentation
- `project_report.md` - full technical documentation (this file)
- `requirements.txt` - Python dependencies
### Dataset Creation
- `dataset_creation/parquet_file_creation.py` - local files to parquet conversion
- `dataset_creation/create_parquet_files_from_owncloud.py` - ownCloud download + parquet conversion
- `dataset_creation/combined_feature_creation.py` - sliding-window multimodal feature generation
- `dataset_creation/maxDist.py` - helper/statistical utility script for eye-tracking feature creation
#### AU Creation
- `dataset_creation/AU_creation/pyfeat_docu.ipynb` - py-feat exploratory notes
#### Camera Handling
- `dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py` - current camera + AU + eye online pipeline
- `dataset_creation/camera_handling/eyeFeature_new.py` - eye-feature extraction from gaze parquet
- `dataset_creation/camera_handling/db_helper.py` - SQLite helper functions (camera pipeline)
- `dataset_creation/camera_handling/camera_stream.py` - baseline camera streaming script
- `dataset_creation/camera_handling/eyeFeature_kalibrierung.py` - eye-feature extraction with calibration
- `dataset_creation/camera_handling/db_test.py` - DB test utility
### EDA
- `EDA/EDA.ipynb` - main EDA notebook
- `EDA/distribution_plots.ipynb` - distribution visualization
- `EDA/histogramms.ipynb` - histogram analysis
- `EDA/researchOnSubjectPerformance.ipynb` - subject-level analysis
- `EDA/owncloud_file_access.ipynb` - ownCloud exploration/access notebook
- `EDA/calculate_replacement_values.ipynb` - fallback/median computation notebook
- `EDA/login.yaml` - local auth/config artifact for EDA workflows
### Model Training
#### CNN
- `model_training/CNN/CNN_simple.ipynb`
- `model_training/CNN/CNN_crossVal.ipynb`
- `model_training/CNN/CNN_crossVal_EarlyFusion.ipynb`
- `model_training/CNN/CNN_crossVal_EarlyFusion_Filter.ipynb`
- `model_training/CNN/CNN_crossVal_EarlyFusion_Test_Eval.ipynb`
- `model_training/CNN/CNN_crossVal_faceAUs.ipynb`
- `model_training/CNN/CNN_crossVal_faceAUs_eyeFeatures.ipynb`
- `model_training/CNN/CNN_crossVal_HybridFusion.ipynb`
- `model_training/CNN/CNN_crossVal_HybridFusion_Test_Eval.ipynb`
- `model_training/CNN/deployment_pipeline.ipynb`
#### XGBoost
- `model_training/xgboost/xgboost.ipynb`
- `model_training/xgboost/xgboost_groupfold.ipynb`
- `model_training/xgboost/xgboost_new_dataset.ipynb`
- `model_training/xgboost/xgboost_regulated.ipynb`
- `model_training/xgboost/xgboost_with_AE.ipynb`
- `model_training/xgboost/xgboost_with_MAD.ipynb`
#### Isolation Forest
- `model_training/IsolationForest/iforest_training.ipynb`
#### OCSVM
- `model_training/OCSVM/ocsvm_with_AE.ipynb`
#### DeepSVDD
- `model_training/DeepSVDD/deepSVDD.ipynb`
#### MAD Outlier Removal
- `model_training/MAD_outlier_removal/mad_outlier_removal.ipynb`
- `model_training/MAD_outlier_removal/mad_outlier_removal_median.ipynb`
#### Shared Training Tools
- `model_training/tools/scaler.py`
- `model_training/tools/performance_split.py`
- `model_training/tools/mad_outlier_removal.py`
- `model_training/tools/evaluation_tools.py`
### Prediction Pipeline
- `predict_pipeline/predict_sample.py` - runtime prediction + MQTT publish
- `predict_pipeline/config.yaml` - runtime database/model/scaler/mqtt config
- `predict_pipeline/fill_db.ipynb` - helper notebook for DB setup/testing
- `predict_pipeline/predict.service` - systemd service unit
- `predict_pipeline/predict.timer` - systemd timer unit
- `predict_pipeline/predict_service_timer_documentation.md` - Linux service/timer guide
### Generic Tools
- `tools/db_helpers.py` - common SQLite utilities used to get newest sample for prediction
## 8) Additional Information
- Several paths are hardcoded on purpose to ensure compability with the jetsonboard at the OHM-UX driving simulator.
- Camera and AU processing are resource-intensive; version pinning and hardware validation are recommended.
- To access our dataset and other valuables, the `data-paulusjafahrsimulator-gpu`
directory contains the raw data, performance results, parquet files, and the final dataset.
+43 -56
View File
@@ -1,67 +1,54 @@
# Multimodal Driver State Analysis
Ein umfassendes Framework zur Analyse von Fahrerverhalten durch kombinierte Feature-Extraktion aus Facial Action Units (AU) und Eye-Tracking Daten.
Short overview: this repository contains the data, feature, training, and inference pipeline for multimodal driver-state analysis using facial AUs and eye-tracking signals.
## 📋 Projektübersicht
For full documentation, see [project_report.md](project_report.md).
Dieses Projekt verarbeitet multimodale Sensordaten aus Fahrsimulator-Studien und extrahiert zeitbasierte Features für die Analyse von Fahrerzuständen. Die Pipeline kombiniert:
## Quickstart
- **Facial Action Units (AU)**: 20 Gesichtsaktionseinheiten zur Emotionserkennung
- **Eye-Tracking**: Fixationen, Sakkaden, Blinks und Pupillenmetriken
## 🎯 Features
### Datenverarbeitung
- **Sliding Window Aggregation**: 50-Sekunden-Fenster mit 5-Sekunden-Schrittweite
- **Hierarchische Gruppierung**: Automatische Segmentierung nach STUDY/LEVEL/PHASE
- **Robuste Fehlerbehandlung**: Graceful Degradation bei fehlenden Modalitäten
### Extrahierte Features
#### Facial Action Units (20 AUs)
Für jede AU wird der Mittelwert pro Window berechnet:
- AU01 (Inner Brow Raiser) bis AU43 (Eyes Closed)
- Aggregation: `mean` über 50s Window
#### Eye-Tracking Features
**Fixationen:**
- Anzahl nach Dauer-Kategorien (66-150ms, 300-500ms, >1000ms, >100ms)
- Mittelwert und Median der Fixationsdauer
**Sakkaden:**
- Anzahl, mittlere Amplitude, mittlere/mediane Dauer
**Blinks:**
- Anzahl, mittlere/mediane Dauer
**Pupille:**
- Mittlere Pupillengröße
- Index of Pupillary Activity (IPA) - Hochfrequenzkomponente (0.6-2.0 Hz)
## 🏗️ Projektstruktur
to be continued.
## 🚀 Installation
### Voraussetzungen
### 1) Setup
Activate the conda-repository "camera_stream_AU_ET_test".
```bash
Python 3.12
conda activate camera_stream_AU_ET_test
```
**Make sure, another environment that fulfills prediction_env.yaml is available**, matching with predict_pipeline/predict.service
See `predict_pipeline/predict_service_timer_documentation.md`
to get an overview over all available conda environments on your device, use this command in anaconda prompt terminal:
```bash
conda info --envs
```
Optionally, create a new environment based on the yaml-file:
```bash
conda env create -f prediction_env.yaml
```
Ohm-UX driving simulator jetson board only: The conda-environment `p310_FS_TF` is used for predictions.
### 2) Camera AU + Eye Pipeline (`camera_stream_AU_and_ET_new.py`)
1. Open `dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py` and adjust:
- `DB_PATH`
- `CAMERA_INDEX`
- `OUTPUT_DIR` (optional)
2. Start camera capture and feature extraction:
```bash
python dataset_creation/camera_handling/camera_stream_AU_and_ET_new.py
```
### Dependencies
3. Stop with `q` in the camera window.
### 3) Predict Pipeline (`predict_pipeline/predict_sample.py`)
1. Edit `predict_pipeline/config.yaml` and set:
- `database.path`, `database.table`, `database.key`
- `model.path`
- `scaler.path` (if `use_scaling: true`)
- MQTT settings under `mqtt`
2. Run one prediction cycle:
```bash
pip install -r requirements.txt
python predict_pipeline/predict_sample.py
```
**Wichtigste Pakete:**
- `pandas`, `numpy` - Datenverarbeitung
- `scipy` - Signalverarbeitung
- `scikit-learn` - Feature-Skalierung & ML
- `pygazeanalyser` - Eye-Tracking Analyse
- `pyarrow` - Parquet I/O
## 💻 Usage
### 1. Feature-Extraktion
to be continued
3. Use [predict_service_timer_documentation.md](/predict_pipeline/predict_service_timer_documentation.md) to see how to use the service and timer for automation. On Ohm-UX driving simulator's jetson board, the service runs in the background and starts automatically when the device is booting.
+27
View File
@@ -0,0 +1,27 @@
# Core data + ML utilities
numpy
pandas
scipy
scikit-learn
pyarrow
joblib
PyYAML
matplotlib
# Prediction pipeline
paho-mqtt
tensorflow
# Camera / feature extraction stack
# It is necessary to create an extra environment for the extraction pipeline, because of the different version needs
Python==3.10
numpy==1.24.4
scipy==1.10.1
opencv-python==4.7.0.72
mediapipe==0.10.13
py-feat
# Data ingestion (ownCloud + HDF)
pyocclient
h5py
tables