AOI Transitions and Dwell Time in Python using Pupil Labs Eye Tracking

This guide explains how to quantify attention inside and outside an AOI using fixation data exported from a Pupil Labs eye tracker.

By the end, you will generate:
- An AOI membership timeline by fixation order
- A dwell-time comparison chart (AOI vs elsewhere)
- A transition matrix showing how attention switches between regions

Note. This code works with Pupil Labs CSV exports (especially fixations.csv). The code uses a panorama image captured in VR

Download Fixations CSV File

Download Panorama Image

Requirements

- Anaconda Python Development Environment
- CSV files exported from Pupil Labs
- Optional panorama image (recommended for visual interpretation)
- Python packages:
  - pandas
  - numpy
  - matplotlib
  - Pillow (usually included with Anaconda; used for reading images and writing image files)

Setup

1. Install Anaconda if needed.
2. Open Spyder, Jupyter Notebook, or VS Code.
3. Put the files in your working folder:
  - fixations.csv (required)
  - optional: a panorama image file (for example, a panorama or screenshot)
4. Save the script as aoi_transitions_dwell.py in the same folder (or a folder of your choice and pass paths on the command line).

To use Spyder, install Anaconda, run it, and launch Spyder. If you see Install instead of Launch for Spyder, install Spyder first. Create a new file in Spyder and save it in the same directory as your data when you run the examples below.

Step 1 Data

We start with fixation events exported by Pupil Labs.

Important variables from fixations.csv:
- start_timestamp: fixation start time (used for temporal order)
- duration: fixation duration in milliseconds (used for dwell-time totals)
- norm_pos_x, norm_pos_y: normalized fixation coordinates (used for AOI classification)
- confidence: fixation confidence (used to optionally filter low-quality fixations)

Step 2 Define AOI Membership

In this step, you:
1. Load fixations.csv
2. Convert normalized fixation coordinates into panorama pixel coordinates
3. Define AOI pixel boundaries
4. Classify each fixation as AOI or Elsewhere

This creates a region label sequence that can be used for dwell and transition analysis.

Panorama image and AOI definition

Download the panorama image for this tutorial provided above.

How this AOI was defined for the tutorial: The panorama used in the reference analysis is 4096 × 2048 pixels (width × height). The avatar region is a rectangle in pixel coordinates of the original image, with the origin at the top-left (x increases to the right, y increases downward): left = 885, right = 1162, top = 873, bottom = 1158. The script scales these numbers to match the actual width and height of your --image file when you run it.

Getting these coordinates in Adobe Illustrator: Open the image at full resolution and confirm the document size in pixels. Set Units to Pixels (for example Edit → Preferences → Units on Windows, or Illustrator → Settings → Units on Mac). Use the Rectangle Tool to draw a box over the region of interest. With the rectangle selected, read X, Y, W, and H in the Transform panel (or use Window → Info while adjusting the shape). Convert to left / right / top / bottom in pixels if needed (for example: right = left + width, bottom = top + height).

Step 2.1 Compute Dwell and Transition Metrics

In this step, you compute region-level behavior metrics from the AOI labels:
1. Total dwell time in AOI versus elsewhere
2. AOI membership timeline over fixation order
3. Transition counts between AOI and elsewhere
4. Transition matrix visualization for quick interpretation

Learning outcome: you can connect where fixations land with how attention shifts over time.

Step 3 Generate AOI Transition and Dwell Outputs

1. Generate AOI membership timeline
2. Generate dwell-time comparison plot
3. Generate AOI transition matrix

Additional optional visuals can be generated to support interpretation.

Step 4 Run the Script

Option A (portable, minimal) — put fixations.csv in the same folder as the script and run:
python aoi_transitions_dwell.py --fixations "fixations.csv"

Option B (custom AOI coordinates)
python aoi_transitions_dwell.py --fixations "fixations.csv" --aoi-left 2933 --aoi-right 3210 --aoi-top 873 --aoi-bottom 1158

Expected output files (saved automatically to your tutorial output folder):
- aoi_membership_timeline.png
- aoi_dwell_time.png
- aoi_transition_matrix.png
- aoi_membership_ribbon.png
- aoi_cumulative_dwell.png
- aoi_duration_strip.png

Step 5 Code

Use the script file aoi_transitions_dwell.py. The script uses fixation timestamps, durations, and normalized fixation coordinates to compute AOI dwell and transition behavior.
                                        
                                            """
                                            @author: Fjorda
                                            """

                                            from __future__ import annotations
                                            import argparse
                                            from pathlib import Path
                                            from typing import Tuple

                                            import matplotlib.pyplot as plt
                                            import numpy as np
                                            import pandas as pd

                                            # Default avatar AOI rectangle in panorama pixel space (original orientation)
                                            IMG_W = 4096
                                            IMG_H = 2048
                                            AOI_X_LEFT = 2933
                                            AOI_X_RIGHT = 3210
                                            AOI_Y_TOP = 873
                                            AOI_Y_BOTTOM = 1158


                                            def parse_args():
                                                script_dir = Path(__file__).resolve().parent
                                                parser = argparse.ArgumentParser(
                                                    description="Analyze AOI dwell time and transitions from Pupil Labs fixations.csv."
                                                )
                                                parser.add_argument("--fixations", type=Path, default=script_dir / "fixations.csv", help="Path to fixations.csv")
                                                parser.add_argument("--image", type=Path, default=None, help="Optional panorama image for overlay plots")
                                                parser.add_argument("--out-dir", type=Path, default=script_dir / "outputs_tutorial4", help="Output directory")
                                                parser.add_argument("--confidence-threshold", type=float, default=0.5, help="Minimum fixation confidence")
                                                parser.add_argument("--max-fixations", type=int, default=200, help="Maximum number of fixations")
                                                parser.add_argument("--img-width", type=int, default=IMG_W, help="Panorama width in pixels")
                                                parser.add_argument("--img-height", type=int, default=IMG_H, help="Panorama height in pixels")
                                                parser.add_argument("--aoi-left", type=float, default=AOI_X_LEFT, help="AOI left x in px")
                                                parser.add_argument("--aoi-right", type=float, default=AOI_X_RIGHT, help="AOI right x in px")
                                                parser.add_argument("--aoi-top", type=float, default=AOI_Y_TOP, help="AOI top y in px")
                                                parser.add_argument("--aoi-bottom", type=float, default=AOI_Y_BOTTOM, help="AOI bottom y in px")
                                                return parser.parse_args()


                                            def load_fixations(path: Path, conf_thr: float, max_fixations: int) -> pd.DataFrame:
                                                df = pd.read_csv(path)
                                                required = {"start_timestamp", "duration", "norm_pos_x", "norm_pos_y"}
                                                missing = required - set(df.columns)
                                                if missing:
                                                    raise ValueError(f"Missing required columns in fixations.csv: {sorted(missing)}")

                                                if "confidence" in df.columns:
                                                    df = df[df["confidence"] >= conf_thr].copy()

                                                df = df.dropna(subset=["start_timestamp", "duration", "norm_pos_x", "norm_pos_y"]).copy()
                                                df = df.sort_values("start_timestamp").reset_index(drop=True)
                                                if max_fixations is not None and len(df) > max_fixations:
                                                    df = df.iloc[:max_fixations].copy()
                                                return df


                                            def fixations_to_pixels(df: pd.DataFrame, w: int, h: int) -> Tuple[np.ndarray, np.ndarray]:
                                                x = np.clip(df["norm_pos_x"].to_numpy(dtype=float), 0.0, 1.0) * (w - 1)
                                                y = (1.0 - np.clip(df["norm_pos_y"].to_numpy(dtype=float), 0.0, 1.0)) * (h - 1)
                                                return x, y


                                            def aoi_calibration_shift(df: pd.DataFrame, img_w: int, img_h: int, left: float, right: float, top: float, bottom: float) -> Tuple[float, float]:
                                                norm_x = np.clip(df["norm_pos_x"].to_numpy(dtype=float), 0.0, 1.0)
                                                norm_y_display = 1.0 - np.clip(df["norm_pos_y"].to_numpy(dtype=float), 0.0, 1.0)
                                                aoi_center_x = ((left + right) / 2.0) / float(img_w)
                                                aoi_center_y = 1.0 - ((top + bottom) / 2.0) / float(img_h)
                                                return aoi_center_x - float(np.median(norm_x)), aoi_center_y - float(np.median(norm_y_display))


                                            def classify_regions(x: np.ndarray, y: np.ndarray, left: float, right: float, top: float, bottom: float) -> np.ndarray:
                                                in_aoi = (x >= left) & (x <= right) & (y >= top) & (y <= bottom)
                                                return np.where(in_aoi, "AOI", "Elsewhere")


                                            def transition_matrix(labels: np.ndarray) -> tuple[np.ndarray, list[str]]:
                                                states = ["AOI", "Elsewhere"]
                                                idx = {s: i for i, s in enumerate(states)}
                                                M = np.zeros((2, 2), dtype=int)
                                                for a, b in zip(labels[:-1], labels[1:]):
                                                    M[idx[a], idx[b]] += 1
                                                return M, states


                                            def transition_counts(labels: np.ndarray) -> dict[tuple[str, str], int]:
                                                keys = [("AOI", "AOI"), ("AOI", "Elsewhere"), ("Elsewhere", "AOI"), ("Elsewhere", "Elsewhere")]
                                                out = {k: 0 for k in keys}
                                                for a, b in zip(labels[:-1], labels[1:]):
                                                    out[(a, b)] += 1
                                                return out


                                            def plot_membership_timeline(labels: np.ndarray, out_path: Path):
                                                seq = (labels == "AOI").astype(int)
                                                x = np.arange(len(seq))
                                                fig, ax = plt.subplots(figsize=(12, 3.8))
                                                ax.step(x, seq, where="post", linewidth=2.0)
                                                ax.set_yticks([0, 1])
                                                ax.set_yticklabels(["Elsewhere", "AOI"])
                                                ax.set_xlabel("Fixation order")
                                                ax.set_ylabel("Region")
                                                ax.set_title("AOI membership timeline")
                                                ax.grid(alpha=0.25)
                                                fig.tight_layout()
                                                fig.savefig(out_path, dpi=180)
                                                plt.close(fig)


                                            def plot_membership_ribbon(labels: np.ndarray, durations_ms: np.ndarray, out_path: Path):
                                                fig, ax = plt.subplots(figsize=(12, 2.6))
                                                durations_s = np.clip(durations_ms.astype(float) / 1000.0, 0.0, None)
                                                starts = np.r_[0.0, np.cumsum(durations_s[:-1])]
                                                colors = np.where(labels == "AOI", "#ef553b", "#636efa")
                                                for s, d, c in zip(starts, durations_s, colors):
                                                    ax.broken_barh([(s, d)], (0.1, 0.8), facecolors=c, edgecolors="white", linewidth=0.35)
                                                ax.set_ylim(0, 1)
                                                ax.set_yticks([])
                                                ax.set_xlabel("Time (seconds)")
                                                ax.set_title("AOI ribbon timeline (duration-weighted)")
                                                ax.grid(axis="x", alpha=0.2)
                                                fig.tight_layout()
                                                fig.savefig(out_path, dpi=180)
                                                plt.close(fig)


                                            def plot_dwell_time(labels: np.ndarray, durations_ms: np.ndarray, out_path: Path):
                                                dwell_aoi = float(np.sum(durations_ms[labels == "AOI"]))
                                                dwell_else = float(np.sum(durations_ms[labels == "Elsewhere"]))

                                                vals_s = np.array([dwell_aoi, dwell_else], dtype=float) / 1000.0
                                                fig, ax = plt.subplots(figsize=(7.2, 4.8))
                                                bars = ax.bar(["AOI", "Elsewhere"], vals_s, color=["#ef553b", "#636efa"], alpha=0.95)
                                                for b in bars:
                                                    h = b.get_height()
                                                    ax.text(b.get_x() + b.get_width() / 2.0, h, f"{h:.2f}s", ha="center", va="bottom", fontsize=10)
                                                ax.set_title("Total dwell time by region")
                                                ax.set_ylabel("Seconds")
                                                ax.grid(axis="y", alpha=0.25)
                                                fig.tight_layout()
                                                fig.savefig(out_path, dpi=180)
                                                plt.close(fig)


                                            def plot_cumulative_dwell(labels: np.ndarray, durations_ms: np.ndarray, out_path: Path):
                                                dur_s = durations_ms.astype(float) / 1000.0
                                                t = np.r_[0.0, np.cumsum(dur_s)]
                                                cum_aoi = np.r_[0.0, np.cumsum(np.where(labels == "AOI", dur_s, 0.0))]
                                                cum_else = np.r_[0.0, np.cumsum(np.where(labels == "Elsewhere", dur_s, 0.0))]

                                                fig, ax = plt.subplots(figsize=(10.8, 4.8))
                                                ax.plot(t, cum_aoi, color="#ef553b", linewidth=2.2, label="AOI")
                                                ax.plot(t, cum_else, color="#636efa", linewidth=2.2, label="Elsewhere")
                                                ax.set_title("Cumulative dwell time")
                                                ax.set_xlabel("Time (seconds)")
                                                ax.set_ylabel("Accumulated dwell (seconds)")
                                                ax.grid(alpha=0.25)
                                                ax.legend(frameon=False)
                                                fig.tight_layout()
                                                fig.savefig(out_path, dpi=180)
                                                plt.close(fig)


                                            def plot_transition_matrix(M: np.ndarray, states: list[str], out_path: Path):
                                                fig, ax = plt.subplots(figsize=(6.8, 5.6))
                                                im = ax.imshow(M, cmap="Blues")
                                                ax.set_xticks(range(len(states)))
                                                ax.set_yticks(range(len(states)))
                                                ax.set_xticklabels(states)
                                                ax.set_yticklabels(states)
                                                ax.set_xlabel("To")
                                                ax.set_ylabel("From")
                                                ax.set_title("AOI transition matrix")

                                                for i in range(M.shape[0]):
                                                    for j in range(M.shape[1]):
                                                        ax.text(j, i, str(int(M[i, j])), ha="center", va="center", color="black")

                                                fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
                                                fig.subplots_adjust(left=0.20, right=0.92, bottom=0.12, top=0.90)
                                                fig.savefig(out_path, dpi=180, bbox_inches="tight", pad_inches=0.03)
                                                plt.close(fig)


                                            def plot_duration_strip(labels: np.ndarray, durations_ms: np.ndarray, out_path: Path):
                                                # Bar chart: average fixation duration by sequence bins and region.
                                                n = len(labels)
                                                bins = min(6, max(3, n // 3 if n >= 9 else 3))
                                                edges = np.linspace(0, n, bins + 1).astype(int)
                                                centers = np.arange(bins, dtype=float)

                                                aoi_means = np.full(bins, np.nan, dtype=float)
                                                else_means = np.full(bins, np.nan, dtype=float)
                                                dur = durations_ms.astype(float)

                                                for i in range(bins):
                                                    lo, hi = edges[i], edges[i + 1]
                                                    if hi <= lo:
                                                        continue
                                                    lab = labels[lo:hi]
                                                    d = dur[lo:hi]
                                                    if np.any(lab == "AOI"):
                                                        aoi_means[i] = float(np.mean(d[lab == "AOI"]))
                                                    if np.any(lab == "Elsewhere"):
                                                        else_means[i] = float(np.mean(d[lab == "Elsewhere"]))

                                                fig, ax = plt.subplots(figsize=(11.2, 4.8))
                                                w = 0.38
                                                x1 = centers - w / 2.0
                                                x2 = centers + w / 2.0
                                                # Replace NaN with 0 for plotting; keep legend and labels clear.
                                                aoi_plot = np.nan_to_num(aoi_means, nan=0.0)
                                                else_plot = np.nan_to_num(else_means, nan=0.0)
                                                ax.bar(x1, aoi_plot, width=w, color="#ef553b", alpha=0.9, label="AOI")
                                                ax.bar(x2, else_plot, width=w, color="#636efa", alpha=0.9, label="Elsewhere")

                                                tick_labels = [f"{edges[i]}-{max(edges[i+1]-1, edges[i])}" for i in range(bins)]
                                                ax.set_xticks(centers)
                                                ax.set_xticklabels(tick_labels)
                                                ax.set_title("Average fixation duration by sequence bin and region")
                                                ax.set_xlabel("Fixation index range")
                                                ax.set_ylabel("Duration (ms)")
                                                ax.grid(axis="y", alpha=0.25)
                                                ax.legend(frameon=False)
                                                fig.tight_layout()
                                                fig.savefig(out_path, dpi=180)
                                                plt.close(fig)


                                            def plot_sankey_like(transitions: dict[tuple[str, str], int], out_path: Path):
                                                # Lightweight Sankey-style figure using thick curved links.
                                                fig, ax = plt.subplots(figsize=(9.2, 5.8))
                                                ax.set_xlim(0, 1)
                                                ax.set_ylim(0, 1)
                                                ax.axis("off")

                                                # Node positions
                                                xL, xR = 0.22, 0.78
                                                yA, yE = 0.72, 0.28
                                                node_w, node_h = 0.08, 0.14
                                                ax.add_patch(plt.Rectangle((xL - node_w / 2, yA - node_h / 2), node_w, node_h, color="#ef553b", alpha=0.9))
                                                ax.add_patch(plt.Rectangle((xL - node_w / 2, yE - node_h / 2), node_w, node_h, color="#636efa", alpha=0.9))
                                                ax.add_patch(plt.Rectangle((xR - node_w / 2, yA - node_h / 2), node_w, node_h, color="#ef553b", alpha=0.9))
                                                ax.add_patch(plt.Rectangle((xR - node_w / 2, yE - node_h / 2), node_w, node_h, color="#636efa", alpha=0.9))
                                                ax.text(xL, 0.92, "From", ha="center", va="center", fontsize=11)
                                                ax.text(xR, 0.92, "To", ha="center", va="center", fontsize=11)
                                                ax.text(xL, yA, "AOI", ha="center", va="center", color="white", fontsize=10, fontweight="bold")
                                                ax.text(xL, yE, "Elsewhere", ha="center", va="center", color="white", fontsize=10, fontweight="bold")
                                                ax.text(xR, yA, "AOI", ha="center", va="center", color="white", fontsize=10, fontweight="bold")
                                                ax.text(xR, yE, "Elsewhere", ha="center", va="center", color="white", fontsize=10, fontweight="bold")

                                                vals = np.array(
                                                    [
                                                        transitions[("AOI", "AOI")],
                                                        transitions[("AOI", "Elsewhere")],
                                                        transitions[("Elsewhere", "AOI")],
                                                        transitions[("Elsewhere", "Elsewhere")],
                                                    ],
                                                    dtype=float,
                                                )
                                                max_v = max(float(vals.max()), 1.0)

                                                def link(y0: float, y1: float, value: float, color: str):
                                                    lw = 2.0 + 16.0 * (value / max_v)
                                                    ax.annotate(
                                                        "",
                                                        xy=(xR - node_w / 2, y1),
                                                        xytext=(xL + node_w / 2, y0),
                                                        arrowprops=dict(
                                                            arrowstyle="-",
                                                            linewidth=lw,
                                                            color=color,
                                                            alpha=0.42,
                                                            connectionstyle="arc3,rad=0.0",
                                                        ),
                                                    )
                                                    ax.text(0.5, (y0 + y1) / 2.0 + (0.03 if y0 > y1 else -0.03), f"{int(value)}", ha="center", va="center", fontsize=9)

                                                link(yA, yA, transitions[("AOI", "AOI")], "#ef553b")
                                                link(yA, yE, transitions[("AOI", "Elsewhere")], "#a855f7")
                                                link(yE, yA, transitions[("Elsewhere", "AOI")], "#10b981")
                                                link(yE, yE, transitions[("Elsewhere", "Elsewhere")], "#636efa")

                                                ax.set_title("Sankey-style AOI transitions", pad=12)
                                                fig.tight_layout()
                                                fig.savefig(out_path, dpi=180)
                                                plt.close(fig)


                                            def plot_transition_network(transitions: dict[tuple[str, str], int], out_path: Path):
                                                fig, ax = plt.subplots(figsize=(7.2, 5.8))
                                                ax.set_xlim(0, 1)
                                                ax.set_ylim(0, 1)
                                                ax.axis("off")

                                                pos = {"AOI": (0.3, 0.5), "Elsewhere": (0.7, 0.5)}
                                                node_color = {"AOI": "#ef553b", "Elsewhere": "#636efa"}
                                                for n, (x, y) in pos.items():
                                                    circ = plt.Circle((x, y), 0.095, color=node_color[n], alpha=0.92)
                                                    ax.add_patch(circ)
                                                    ax.text(x, y, n, ha="center", va="center", color="white", fontweight="bold")

                                                max_v = max(float(max(transitions.values())), 1.0)
                                                pairs = [("AOI", "AOI"), ("AOI", "Elsewhere"), ("Elsewhere", "AOI"), ("Elsewhere", "Elsewhere")]
                                                for a, b in pairs:
                                                    v = float(transitions[(a, b)])
                                                    lw = 1.5 + 10.0 * (v / max_v)
                                                    if a == b:
                                                        x, y = pos[a]
                                                        loop = plt.Circle((x, y + 0.17), 0.06, fill=False, linewidth=lw, color="#94a3b8", alpha=0.7)
                                                        ax.add_patch(loop)
                                                        ax.text(x, y + 0.27, f"{int(v)}", ha="center", va="center", fontsize=9)
                                                    else:
                                                        xa, ya = pos[a]
                                                        xb, yb = pos[b]
                                                        rad = 0.2 if a == "AOI" else -0.2
                                                        ax.annotate(
                                                            "",
                                                            xy=(xb, yb),
                                                            xytext=(xa, ya),
                                                            arrowprops=dict(arrowstyle="->", linewidth=lw, color="#475569", alpha=0.75, connectionstyle=f"arc3,rad={rad}"),
                                                        )
                                                        ax.text(0.5, 0.63 if a == "AOI" else 0.37, f"{int(v)}", ha="center", va="center", fontsize=9)

                                                ax.set_title("Transition network graph")
                                                fig.tight_layout()
                                                fig.savefig(out_path, dpi=180)
                                                plt.close(fig)


                                            def draw_aoi_rect(ax, left: float, right: float, top: float, bottom: float):
                                                ax.add_patch(
                                                    plt.Rectangle(
                                                        (left, top),
                                                        right - left,
                                                        bottom - top,
                                                        fill=False,
                                                        edgecolor="white",
                                                        linewidth=2.2,
                                                        linestyle="--",
                                                    )
                                                )


                                            def main():
                                                args = parse_args()
                                                fix_path = args.fixations.expanduser().resolve()
                                                out_dir = args.out_dir.expanduser().resolve()
                                                out_dir.mkdir(parents=True, exist_ok=True)

                                                if not fix_path.exists():
                                                    raise FileNotFoundError(f"Missing fixations file: {fix_path}")

                                                df = load_fixations(fix_path, args.confidence_threshold, args.max_fixations)
                                                left = min(args.aoi_left, args.aoi_right)
                                                right = max(args.aoi_left, args.aoi_right)
                                                top = min(args.aoi_top, args.aoi_bottom)
                                                bottom = max(args.aoi_top, args.aoi_bottom)

                                                shift_x, shift_y = aoi_calibration_shift(df, args.img_width, args.img_height, left, right, top, bottom)
                                                x, y = fixations_to_pixels(df, args.img_width, args.img_height)
                                                x = np.clip(x + shift_x * (args.img_width - 1), 0.0, args.img_width - 1)
                                                y = np.clip(y + shift_y * (args.img_height - 1), 0.0, args.img_height - 1)

                                                labels = classify_regions(
                                                    x=x,
                                                    y=y,
                                                    left=left,
                                                    right=right,
                                                    top=top,
                                                    bottom=bottom,
                                                )

                                                M, states = transition_matrix(labels)
                                                transitions = transition_counts(labels)
                                                durations_ms = df["duration"].to_numpy(dtype=float)

                                                plot_membership_timeline(labels, out_dir / "aoi_membership_timeline.png")
                                                plot_membership_ribbon(labels, durations_ms, out_dir / "aoi_membership_ribbon.png")
                                                plot_dwell_time(labels, durations_ms, out_dir / "aoi_dwell_time.png")
                                                plot_cumulative_dwell(labels, durations_ms, out_dir / "aoi_cumulative_dwell.png")
                                                plot_transition_matrix(M, states, out_dir / "aoi_transition_matrix.png")
                                                plot_duration_strip(labels, durations_ms, out_dir / "aoi_duration_strip.png")
                                                # Sankey-style output intentionally omitted for this tutorial version.
                                                # We intentionally skip panorama fixation map here because this visualization
                                                # is already covered in previous tutorials.

                                                print(f"Fixations used: {len(df):,}")
                                                print(f"AOI fixations: {int(np.sum(labels == 'AOI')):,}")
                                                print(f"Outputs saved to: {out_dir}")


                                            if __name__ == "__main__":
                                                main()


                                            
                                        
                                    
After you run the script, you will obtain the AOI transition and dwell outputs listed in Step 4. Example figures below keep the same image paths format used on this site (replace with your final Tutorial 4 assets when uploading).

Conclusions

  • AOI membership labels convert fixation streams into interpretable regions.
  • Dwell-time summaries quantify how much attention each region receives.
  • Transition metrics reveal how attention switches between AOI and elsewhere over time.