Created
August 13, 2024 09:43
-
-
Save nadje/7a8f1e5590ab9022be4253a5b0d8d238 to your computer and use it in GitHub Desktop.
Generate_Scanpaths.ipynb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "metadata": { | |
| "id": "view-in-github", | |
| "colab_type": "text" | |
| }, | |
| "source": [ | |
| "<a href=\"https://colab.research.google.com/gist/nadje/7a8f1e5590ab9022be4253a5b0d8d238/generate_scanpaths.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "id": "WRuXzfG2dH_Q" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title # Welcome to the Google Colab Notebook! { vertical-output: true, display-mode: \"form\" }\n", | |
| "#@markdown This notebook offers a straightforward, nearly click-and-run method for creating scanpaths on images.\n", | |
| "\n", | |
| "#@markdown To get started, please upload the export folder from Pupil Cloud's Manual Mapper or Reference Image Mapper to your Google Drive.\n", | |
| "\n", | |
| "#@markdown **Ready to run this notebook?** Simply click on the play buttons next to each cell." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "id": "LAAddzKrdQS4" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title # Import Packages and Connect to Google Drive { vertical-output: true, display-mode: \"form\" }\n", | |
| "\n", | |
| "#@markdown First, let's import some packages and connect to our Google Drive.\n", | |
| "\n", | |
| "#@markdown Run the following cell and a pop-up window will appear, prompting you to connect to your Drive.\n", | |
| "\n", | |
| "!pip install matplotlib pillow requests numpy pandas google-colab ipywidgets opencv-python tqdm rich moviepy\n", | |
| "\n", | |
| "# Import necessary libraries\n", | |
| "import matplotlib.patches as patches\n", | |
| "import matplotlib.pyplot as plt\n", | |
| "import matplotlib.animation as animation\n", | |
| "from google.colab import drive\n", | |
| "from ipywidgets import widgets\n", | |
| "from PIL import Image\n", | |
| "from tqdm import tqdm\n", | |
| "from pathlib import Path\n", | |
| "from rich.logging import RichHandler\n", | |
| "from moviepy.editor import ImageClip\n", | |
| "from contextlib import contextmanager\n", | |
| "from google.colab import files\n", | |
| "from IPython.display import display\n", | |
| "from getpass import getpass\n", | |
| "import requests\n", | |
| "import json\n", | |
| "import glob\n", | |
| "import numpy as np\n", | |
| "import pandas as pd\n", | |
| "import random\n", | |
| "import re\n", | |
| "import os\n", | |
| "import cv2\n", | |
| "import math\n", | |
| "import shutil\n", | |
| "import logging\n", | |
| "import warnings\n", | |
| "\n", | |
| "\n", | |
| "# Suppress all warnings\n", | |
| "warnings.filterwarnings('ignore')\n", | |
| "\n", | |
| "log = logging.getLogger(__name__)\n", | |
| "logging.basicConfig(level=logging.INFO)\n", | |
| "\n", | |
| "\n", | |
| "# Mount Google Drive\n", | |
| "drive.mount('/content/drive')\n", | |
| "\n", | |
| "# Configure logging\n", | |
| "logging.basicConfig(level=logging.INFO, handlers=[RichHandler()])\n", | |
| "\n", | |
| "# Set seed for reproducibility\n", | |
| "random.seed(42)\n", | |
| "\n", | |
| "print(\"All packages imported and Google Drive mounted successfully.\")\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "id": "VyFHJ47Cdfs0" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title # Retrieve Enrichment Information { vertical-output: true, display-mode: \"form\" }\n", | |
| "\n", | |
| "#@markdown Data from your enrichment needs to be loaded onto Google Drive. You can either:\n", | |
| "#@markdown * Download the enrichment manually, then upload it to Google Drive and specify the folder here, or\n", | |
| "#@markdown * Use a Pupil Cloud API token to have the enrichment loaded into Google Drive by this notebook\n", | |
| "\n", | |
| "#@markdown Run this cell first, then complete the form\n", | |
| "\n", | |
| "def extract_from_url(url: str):\n", | |
| " import re\n", | |
| " base_url = re.match(r'https?://[^/]+', url).group(0)\n", | |
| "\n", | |
| " segments = {\n", | |
| " \"workspace_id\": \"/workspaces/\",\n", | |
| " \"project_id\": \"/projects/\",\n", | |
| " \"enrichment_id\": \"/enrichments/\",\n", | |
| " }\n", | |
| "\n", | |
| " extracted_values = {}\n", | |
| "\n", | |
| " for key, segment in segments.items():\n", | |
| " match = re.search(f\"{segment}([^/]+)\", url)\n", | |
| " if match:\n", | |
| " extracted_values[key] = match.group(1)\n", | |
| " else:\n", | |
| " extracted_values[key] = None\n", | |
| "\n", | |
| " enrichment_ID = extracted_values['enrichment_id']\n", | |
| " workspace_ID = extracted_values['workspace_id']\n", | |
| " project_ID = extracted_values['project_id']\n", | |
| " return enrichment_ID, workspace_ID, project_ID\n", | |
| "\n", | |
| "def download_url(path: str, save_path: str, api_key: str, chunk_size: int = 128) -> int:\n", | |
| " \"\"\"Download file from given API path and save to specified location.\"\"\"\n", | |
| " API_URL = \"https://api.cloud.pupil-labs.com/v2\"\n", | |
| " url = f\"{API_URL}/{path}\"\n", | |
| "\n", | |
| " response = requests.get(url, stream=True, headers={\"api-key\": api_key})\n", | |
| " response.raise_for_status() # Raise exception for bad status codes\n", | |
| "\n", | |
| " save_path = Path(save_path)\n", | |
| " with open(save_path, 'wb') as file:\n", | |
| " for chunk in response.iter_content(chunk_size=chunk_size):\n", | |
| " file.write(chunk)\n", | |
| "\n", | |
| " return response.status_code\n", | |
| "\n", | |
| "def api_get(path: str, api_key: str) -> dict:\n", | |
| " \"\"\"Fetch JSON data from the given API path.\"\"\"\n", | |
| " API_URL = \"https://api.cloud.pupil-labs.com/v2\"\n", | |
| " url = f\"{API_URL}/{path}\"\n", | |
| "\n", | |
| " response = requests.get(url, headers={\"api-key\": api_key})\n", | |
| " data = response.json()\n", | |
| "\n", | |
| " if data[\"status\"] == \"success\":\n", | |
| " return data[\"result\"]\n", | |
| " else:\n", | |
| " log.error(data[\"message\"])\n", | |
| " raise Exception(data[\"message\"])\n", | |
| "\n", | |
| "def get_enrichment_dict(enrichment_id: str, project_id: str, workspace_id: str, api_key: str, save_path: Path) -> (dict, Path):\n", | |
| " \"\"\"Download and extract enrichment data from Pupil Cloud.\"\"\"\n", | |
| " log.info(f\"Fetching enrichment: {enrichment_id}\")\n", | |
| "\n", | |
| " # Define the download path and ensure the directory exists\n", | |
| " download_path = Path(f\"/content/drive/My Drive/enrichment_{enrichment_id}\")\n", | |
| " download_path.mkdir(parents=True, exist_ok=True)\n", | |
| "\n", | |
| " # Download the enrichment export\n", | |
| " download_url(f\"/workspaces/{workspace_id}/projects/{project_id}/enrichments/{enrichment_id}/export\", save_path, api_key)\n", | |
| "\n", | |
| " # Extract the downloaded archive\n", | |
| " shutil.unpack_archive(save_path, download_path)\n", | |
| " os.remove(save_path)\n", | |
| "\n", | |
| " return {}, download_path\n", | |
| "\n", | |
| "def retrieve_enrichment():\n", | |
| " global filepaths\n", | |
| " # Extract IDs from the provided URL\n", | |
| " enrichment_ID, workspace_ID, project_ID = extract_from_url(cloud_url_widget.value)\n", | |
| "\n", | |
| " # Display the extracted IDs\n", | |
| " print(f\"Workspace ID: {workspace_ID}\")\n", | |
| " print(f\"Project ID: {project_ID}\")\n", | |
| " print(f\"Enrichment ID: {enrichment_ID}\")\n", | |
| "\n", | |
| " pupil_cloud_token = api_token_widget.value\n", | |
| "\n", | |
| " # User inputs\n", | |
| " save_path = Path(f\"/content/drive/My Drive/enrichment_{enrichment_ID}.zip\")\n", | |
| " drive_path_widget.value = str(save_path)\n", | |
| " enrichment, enrichment_folder = get_enrichment_dict(enrichment_ID, project_ID, workspace_ID, pupil_cloud_token, save_path)\n", | |
| " print(f\"Enrichment folder: {enrichment_folder}\")\n", | |
| " print(\"Enrichment has been downloaded successfully.\")\n", | |
| "\n", | |
| "def update_file_mode_options(change):\n", | |
| " auto_retrieve = change['new']\n", | |
| " if auto_retrieve:\n", | |
| " cloud_options_container.layout.display = ''\n", | |
| " drive_path_widget.disabled = True\n", | |
| " else:\n", | |
| " cloud_options_container.layout.display = 'none'\n", | |
| " drive_path_widget.disabled = False\n", | |
| "\n", | |
| "\n", | |
| "auto_retrieve_widget = widgets.Checkbox(\n", | |
| " description='Retrieve the enrichment for me',\n", | |
| " indent=False,\n", | |
| " value=True\n", | |
| ")\n", | |
| "auto_retrieve_widget.observe(update_file_mode_options, names='value')\n", | |
| "\n", | |
| "# cloud options\n", | |
| "api_token_widget = widgets.Password()\n", | |
| "\n", | |
| "cloud_url_widget = widgets.Text()\n", | |
| "download_button = widgets.Button(description='Retrieve enrichment')\n", | |
| "\n", | |
| "download_button.on_click(lambda _: retrieve_enrichment())\n", | |
| "\n", | |
| "cloud_options_container = widgets.GridBox(\n", | |
| " layout=widgets.Layout(grid_template_columns='150px 1fr'),\n", | |
| " children=[\n", | |
| " widgets.Label(\"Pupil Cloud API Token\"), api_token_widget,\n", | |
| " widgets.Label(\"Enrichment URL\"), cloud_url_widget,\n", | |
| " widgets.Label(\"\"), download_button,\n", | |
| " ],\n", | |
| ")\n", | |
| "\n", | |
| "# manual upload/download options\n", | |
| "drive_path_widget = widgets.Text()\n", | |
| "drive_options_container = widgets.GridBox(\n", | |
| " layout=widgets.Layout(grid_template_columns='150px 4fr'),\n", | |
| " children=[\n", | |
| " widgets.Label(\"Google Drive Path\"), drive_path_widget,\n", | |
| " ],\n", | |
| ")\n", | |
| "drive_path_widget.disabled = True\n", | |
| "\n", | |
| "display(auto_retrieve_widget)\n", | |
| "display(cloud_options_container)\n", | |
| "display(drive_options_container)\n", | |
| "\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "id": "xTH41KVXVGHW" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title #Select Your Scanpath Parameters { vertical-output: true, display-mode: \"form\" }\n", | |
| "\n", | |
| "# Set circle color and fixation ID font size based on user input\n", | |
| "fixation_id_font_size = 5 #@param {type:\"integer\"}\n", | |
| "\n", | |
| "minimum_size_fixation_circle = 5 #@param {type:\"integer\"}\n", | |
| "maximum_size_fixation_circle = 80 #@param {type:\"integer\"}\n", | |
| "\n", | |
| "\n", | |
| "fixation_selection = \"Plot only first 10 fixations\" #@param [\"Plot all fixations\", \"Plot only first 5 fixations\", \"Plot only last 5 fixations\", \"Plot only first 10 fixations\", \"Plot only last 10 fixations\"] {type:\"string\"}\n", | |
| "if fixation_selection == \"Plot all fixations\":\n", | |
| " num_fixations = None\n", | |
| "elif fixation_selection == \"Plot only first 5 fixations\":\n", | |
| " num_fixations = 5\n", | |
| " head = True\n", | |
| "elif fixation_selection == \"Plot only last 5 fixations\":\n", | |
| " num_fixations = 5\n", | |
| " head = False\n", | |
| "elif fixation_selection == \"Plot only first 10 fixations\":\n", | |
| " num_fixations = 10\n", | |
| " head = True\n", | |
| "elif fixation_selection == \"Plot only last 10 fixations\":\n", | |
| " num_fixations = 10\n", | |
| " head = False\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "id": "hSmNC8OfA7g3" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title #Select The Wearers To Be Included { vertical-output: true, display-mode: \"form\" }\n", | |
| "non_zipped = Path(drive_path_widget.value).with_suffix('')\n", | |
| "enrichment_folder = Path('/content/drive/My Drive' / non_zipped)\n", | |
| "\n", | |
| "# Specify the files to include\n", | |
| "files_to_include = [\"fixations.csv\", \"sections.csv\"]\n", | |
| "\n", | |
| "# Create a dictionary with file paths\n", | |
| "filepaths = {file_name: enrichment_folder / file_name for file_name in files_to_include}\n", | |
| "\n", | |
| "# Load data frames\n", | |
| "fixations_df = pd.read_csv(filepaths[\"fixations.csv\"])\n", | |
| "sections_df = pd.read_csv(filepaths[\"sections.csv\"])\n", | |
| "# Define selected columns\n", | |
| "selected_col = [\"recording id\", \"wearer id\", \"wearer name\"]\n", | |
| "\n", | |
| "all_data = pd.merge(\n", | |
| " sections_df[selected_col],\n", | |
| " fixations_df, # Select only the required columns from fixations_df\n", | |
| " on=\"recording id\", # Merge on \"recording id\" column\n", | |
| " how=\"inner\"\n", | |
| ")\n", | |
| "\n", | |
| "# Extract unique wearer names\n", | |
| "unique_wearer_names = all_data['wearer name'].unique()\n", | |
| "unique_wearer_names\n", | |
| "# Define a function to update unique_wearer_names based on selected wearers\n", | |
| "def update_unique_wearer_names(change):\n", | |
| " global unique_wearer_names\n", | |
| " unique_wearer_names = list(change['new'])\n", | |
| "# Create multi-select dropdown widget\n", | |
| "wearers_dropdown = widgets.SelectMultiple(\n", | |
| " options=unique_wearer_names,\n", | |
| " value=tuple(unique_wearer_names), # Convert to tuple\n", | |
| " description='Select Wearers:',\n", | |
| " disabled=False\n", | |
| ")\n", | |
| "\n", | |
| "# Define observer to update unique_wearer_names when selection changes\n", | |
| "wearers_dropdown.observe(update_unique_wearer_names, names='value')\n", | |
| "\n", | |
| "# Display the widget\n", | |
| "display(wearers_dropdown)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "id": "IaZo9zSQA-sB", | |
| "cellView": "form" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title # Load The Reference Image\n", | |
| "from pathlib import Path\n", | |
| "\n", | |
| "def get_image_path(base_filename):\n", | |
| " extensions = ['jpeg', 'png', 'jpg']\n", | |
| " for ext in extensions:\n", | |
| " file_path = Path(enrichment_folder) / f\"{base_filename}.{ext}\"\n", | |
| " if os.path.exists(file_path):\n", | |
| " return file_path\n", | |
| " raise FileNotFoundError(f\"No file found for {base_filename} with extensions {extensions}\")\n", | |
| "\n", | |
| "\n", | |
| "# Example usage\n", | |
| "base_filename = \"reference_image\"\n", | |
| "image_path = get_image_path(base_filename)\n", | |
| "print(f\"The image is found in this path: {image_path}\")\n" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "source": [ | |
| "#@title #Generate Static Scanpaths { vertical-output: true, display-mode: \"form\" }\n", | |
| "min_duration = all_data['duration [ms]'].min() # Minimum duration\n", | |
| "max_duration = all_data['duration [ms]'].max() # Maximum duration\n", | |
| "\n", | |
| "# Generate a list of random colors\n", | |
| "random.seed(42) # Set seed for reproducibility\n", | |
| "num_unique_colors = len(unique_wearer_names)\n", | |
| "random_colors = [plt.cm.viridis(random.random()) for _ in range(num_unique_colors)]\n", | |
| "\n", | |
| "img = Image.open(image_path)\n", | |
| "dpi=400\n", | |
| "# Create a figure and axis object\n", | |
| "fig, ax = plt.subplots(dpi=dpi, figsize=(img.width/dpi, img.height/dpi)) # Match figure size to the image size\n", | |
| "\n", | |
| "# Plot the image\n", | |
| "ax.imshow(img)\n", | |
| "\n", | |
| "# make a consistent column name for fixation status\n", | |
| "if 'fixation status' in all_data:\n", | |
| " all_data['_fixated'] = all_data['fixation status'] == 'true'\n", | |
| "else:\n", | |
| " all_data['_fixated'] = all_data['fixation detected in reference image'] == True\n", | |
| "\n", | |
| "# Plot fixations on the image for each unique wearer\n", | |
| "for idx, wearer_name in enumerate(unique_wearer_names):\n", | |
| " wearer_data = all_data[(all_data['wearer name'] == wearer_name) & (all_data['_fixated'])]\n", | |
| "\n", | |
| " # Define marker color for the current wearer\n", | |
| " color = random_colors[idx] # Use random color from the list\n", | |
| "\n", | |
| " # Select fixations based on user input\n", | |
| " if num_fixations is not None:\n", | |
| " if head == True:\n", | |
| " wearer_data = wearer_data.head(num_fixations)\n", | |
| " else:\n", | |
| " wearer_data = wearer_data.tail(num_fixations)\n", | |
| "\n", | |
| " # Plot fixations on the image\n", | |
| " for index in range(len(wearer_data)):\n", | |
| " row = wearer_data.iloc[index]\n", | |
| " # Extract fixation coordinates\n", | |
| " x = row['fixation x [px]']\n", | |
| " y = row['fixation y [px]']\n", | |
| " fixation_id = row['fixation id'] # Assuming the column name is 'fixation id'\n", | |
| "\n", | |
| " # Calculate marker size based on duration\n", | |
| " duration = row['duration [ms]']\n", | |
| " marker_size = np.interp(duration, [min_duration, max_duration], [minimum_size_fixation_circle, maximum_size_fixation_circle])\n", | |
| "\n", | |
| " # Plot fixation point with adjusted size, blue color, and transparency\n", | |
| " ax.plot(x, y, 'o', markersize=marker_size, markerfacecolor=color, markeredgecolor='black', alpha=0.5)\n", | |
| "\n", | |
| " # Add fixation ID next to the circle with smaller font size\n", | |
| " ax.annotate(fixation_id, (x, y), textcoords=\"offset points\", xytext=(5, 5), ha='center', fontsize=fixation_id_font_size, color='black')\n", | |
| "\n", | |
| " # Draw scanpath line only if fixation IDs are consecutive\n", | |
| " if index > 0:\n", | |
| " prev_row = wearer_data.iloc[index - 1]\n", | |
| " prev_fixation_id = prev_row['fixation id']\n", | |
| " if fixation_id == prev_fixation_id + 1:\n", | |
| " prev_x, prev_y = prev_row['fixation x [px]'], prev_row['fixation y [px]']\n", | |
| " ax.plot([prev_x, x], [prev_y, y], 'black', linewidth=0.5)\n", | |
| "\n", | |
| " # Add legend entry for the current wearer\n", | |
| " ax.plot([], [], 'o', markersize=15, markerfacecolor=color, markeredgecolor='none', alpha=0.7, label=wearer_name)\n", | |
| "\n", | |
| "# Add legend\n", | |
| "ax.legend(loc='upper left')\n", | |
| "\n", | |
| "# Turn off axes\n", | |
| "ax.axis('off')\n", | |
| "\n", | |
| "# Show the plot\n", | |
| "plt.show()\n", | |
| "\n", | |
| "# Save the plot as an image file in the specified folder\n", | |
| "try:\n", | |
| " fig.savefig(os.path.join(enrichment_folder, \"scanpath_image.png\"), bbox_inches='tight')\n", | |
| " print(\"Image saved successfully.\")\n", | |
| "except Exception as e:\n", | |
| " print(\"An error occurred while saving the image:\", e)" | |
| ], | |
| "metadata": { | |
| "id": "S3kAbG7LDhNR" | |
| }, | |
| "execution_count": null, | |
| "outputs": [] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "metadata": { | |
| "id": "wc2HgAhzFqZK" | |
| }, | |
| "outputs": [], | |
| "source": [ | |
| "#@title #Generate Dynamic Scanpaths { vertical-output: true, display-mode: \"form\" }\n", | |
| "\n", | |
| "# Generate a list of random colors\n", | |
| "random.seed(42) # Set seed for reproducibility\n", | |
| "num_unique_colors = len(unique_wearer_names)\n", | |
| "random_colors = [plt.cm.viridis(random.random()) for _ in range(num_unique_colors)]\n", | |
| "\n", | |
| "img = Image.open(image_path)\n", | |
| "\n", | |
| "# Plotting variables\n", | |
| "min_duration = all_data['duration [ms]'].min()\n", | |
| "max_duration = all_data['duration [ms]'].max()\n", | |
| "\n", | |
| "def create_scanpath_video(wearer_name, wearer_data, color):\n", | |
| " # Initialize plot elements that will change\n", | |
| " plot_elements = [(wearer_data, color)]\n", | |
| "\n", | |
| " # Create a figure and axis object with higher resolution\n", | |
| " fig, ax = plt.subplots(dpi=dpi, figsize=(img.width/dpi, img.height/dpi)) # Match figure size to the image size\n", | |
| " ax.imshow(img)\n", | |
| "\n", | |
| " def init():\n", | |
| " ax.imshow(img)\n", | |
| " return []\n", | |
| "\n", | |
| " # Keep track of all fixations and their durations\n", | |
| " all_fixations = []\n", | |
| " def update(frame):\n", | |
| " ax.clear()\n", | |
| " ax.imshow(img)\n", | |
| " # Turn off axes\n", | |
| " ax.axis('off')\n", | |
| "\n", | |
| " # Plot fixations on the image\n", | |
| " for wearer_data, color in plot_elements:\n", | |
| " if frame < len(wearer_data):\n", | |
| " row = wearer_data.iloc[frame]\n", | |
| " x, y = row['fixation x [px]'], row['fixation y [px]']\n", | |
| " duration = row['duration [ms]']\n", | |
| " marker_size = np.interp(duration, [min_duration, max_duration], [minimum_size_fixation_circle, maximum_size_fixation_circle])\n", | |
| " fixation_id = row['fixation id']\n", | |
| " all_fixations.append((x, y, marker_size, color, fixation_id, duration))\n", | |
| "\n", | |
| " # Plot all fixations and lines\n", | |
| " for i, (x, y, marker_size, color, fixation_id, duration) in enumerate(all_fixations):\n", | |
| " if i > frame:\n", | |
| " break\n", | |
| " ax.plot(x, y, 'o', markersize=marker_size, markerfacecolor=color, markeredgecolor='black', alpha=0.5)\n", | |
| " ax.annotate(fixation_id, (x, y), textcoords=\"offset points\", xytext=(5, 5), ha='center', fontsize=fixation_id_font_size, color='black')\n", | |
| "\n", | |
| " # Draw scanpath line only if fixation IDs are consecutive\n", | |
| " if i > 0:\n", | |
| " prev_row = wearer_data.iloc[i - 1]\n", | |
| " prev_fixation_id = prev_row['fixation id']\n", | |
| " if fixation_id == prev_fixation_id + 1:\n", | |
| " prev_x, prev_y = prev_row['fixation x [px]'], prev_row['fixation y [px]']\n", | |
| " ax.plot([prev_x, x], [prev_y, y], 'black', linewidth=0.5)\n", | |
| "\n", | |
| " # if i > 0:\n", | |
| " # prev_x, prev_y = all_fixations[i-1][:2]\n", | |
| " # ax.plot([prev_x, x], [prev_y, y], 'black', linewidth=0.5)\n", | |
| "\n", | |
| " return []\n", | |
| "\n", | |
| " # Adjust layout to eliminate margins\n", | |
| " plt.subplots_adjust(left=0, right=1, top=1, bottom=0)\n", | |
| "\n", | |
| " # Create animation\n", | |
| " num_frames = len(wearer_data)\n", | |
| " ani = animation.FuncAnimation(fig, update, frames=num_frames, init_func=init, blit=True, repeat=False)\n", | |
| "\n", | |
| " # Save the animation as a video file with higher resolution\n", | |
| " video_path = os.path.join(enrichment_folder, f\"scanpath_video_{wearer_name}.mp4\")\n", | |
| " ani.save(video_path, writer='ffmpeg', fps=1, dpi=dpi) # Adjust fps and dpi as needed\n", | |
| "\n", | |
| " plt.close(fig)\n", | |
| "\n", | |
| "if 'fixation status' in all_data:\n", | |
| " all_data['_fixated'] = all_data['fixation status'] == 'true'\n", | |
| "else:\n", | |
| " all_data['_fixated'] = all_data['fixation detected in reference image'] == True\n", | |
| "\n", | |
| "# Iterate over each unique wearer and create scanpath videos\n", | |
| "for idx, wearer_name in enumerate(unique_wearer_names):\n", | |
| " color = random_colors[idx] # Use random color from the list\n", | |
| " wearer_data = all_data[(all_data['wearer name'] == wearer_name) & (all_data['_fixated'])]\n", | |
| "\n", | |
| " if num_fixations is not None:\n", | |
| " if head == True:\n", | |
| " wearer_data = wearer_data.head(num_fixations)\n", | |
| " else:\n", | |
| " wearer_data = wearer_data.tail(num_fixations)\n", | |
| " create_scanpath_video(wearer_name, wearer_data, color)\n", | |
| " print(f\"Scanpath video for {wearer_name} created.\")" | |
| ] | |
| } | |
| ], | |
| "metadata": { | |
| "colab": { | |
| "provenance": [], | |
| "include_colab_link": true | |
| }, | |
| "kernelspec": { | |
| "display_name": "Python 3", | |
| "name": "python3" | |
| }, | |
| "language_info": { | |
| "name": "python" | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 0 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment