{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CMI: Dead Reckoning 3D Gesture Reconstruction\n\nI thought it might be interesting to try to reconstruct the gesture paths from IU data using dead reckoning...\n\nBe sure to **hit \"Play\"** to see the gestures animate!\n\nThe animations are all 3D - and can be rotated / zoomed.\n\nFor all animations:\n* Red is \"moves hand to target location\" (or 1st behavior)\n* Green is \"hand at target location\" (or 2nd behavior)\n* Blue is always \"Performs Gesture\"\n\nA fun game might be to see if we can figure out one of the subject's first names!\n\nNote: I was hoping to provide more samples of gestures in this notebook - but the animations are **very memory intensive**.  I needed to keep the number of examples down to prevent **browser crashes**.  You can easily fork and modify this notebook to investigate specific gestures in detail.","metadata":{}},{"cell_type":"code","source":"!pip install ahrs -q\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom scipy.signal import butter, filtfilt\n\nfrom scipy.integrate import cumulative_trapezoid\nfrom scipy.spatial.transform import Rotation as R\n\n# Madgwick filter (only needed if gyro present & quaternions absent)\nfrom ahrs.filters import Madgwick\n\nimport plotly.graph_objects as go\nimport plotly.express as px\nimport plotly.io as pio\n\nkaggle_kernel_run_type = os.environ.get('KAGGLE_KERNEL_RUN_TYPE', '')\nif kaggle_kernel_run_type != 'Interactive': \n    pio.renderers.default = \"notebook\"\nprint(\"Pio renderer set to:\", pio.renderers.default)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:11:52.855866Z","iopub.execute_input":"2025-06-14T07:11:52.856229Z","iopub.status.idle":"2025-06-14T07:11:55.840168Z","shell.execute_reply.started":"2025-06-14T07:11:52.856210Z","shell.execute_reply":"2025-06-14T07:11:55.839191Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"raw = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:11:55.842199Z","iopub.execute_input":"2025-06-14T07:11:55.842454Z","iopub.status.idle":"2025-06-14T07:12:15.577031Z","shell.execute_reply.started":"2025-06-14T07:11:55.842431Z","shell.execute_reply":"2025-06-14T07:12:15.576088Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dead Reckoning Function\n* Transforms IMU data into trajectories by double-integrating accelerations and orientation data through velocities\n* Uses high-pass filtering to reduce drift and orientation processing to remove gravity\n* Try changing HIGHPASS_CUTOFF_HZ / FILTER_ORDER to adjust smoothness of output\n* Converts our IMU data into gesture paths!","metadata":{}},{"cell_type":"code","source":"ACC_COLS = [\"acc_x\", \"acc_y\", \"acc_z\"]\nORI_COLS = [\"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\n\ndef quaternion_to_rotation_matrix(q):\n    \"\"\"Convert quaternion to rotation matrix.\"\"\"\n    w, x, y, z = q\n    return np.array([\n        [1 - 2*(y**2 + z**2), 2*(x*y - w*z), 2*(x*z + w*y)],\n        [2*(x*y + w*z), 1 - 2*(x**2 + z**2), 2*(y*z - w*x)],\n        [2*(x*z - w*y), 2*(y*z + w*x), 1 - 2*(x**2 + y**2)]\n    ])\n\ndef integrate_imu_sequence(imu, *, verbose: bool = False, enable_filtering: bool = True, use_orientation: bool = True):\n    \"\"\"\n    Integrate IMU data using raw AC acceleration signal with optional orientation support.\n    \n    Args:\n        imu (pd.DataFrame): IMU data for a single sequence.\n        verbose (bool, optional): If True, print diagnostic information.\n                                  Defaults to False.\n        enable_filtering (bool, optional): If True, apply high-pass filtering.\n                                         If False, use raw integration (will drift).\n                                         Defaults to True.\n        use_orientation (bool, optional): If True, use quaternion data to transform\n                                        to world frame and remove gravity.\n                                    \n    Returns\n    -------\n    dict\n        Dictionary containing position, velocity, acceleration, and metadata.\n    \"\"\"\n    \n    # === CONFIGURATION PARAMETERS ===\n    # High-pass filter settings (only used if enable_filtering=True)\n    HIGHPASS_CUTOFF_HZ = 3.0    # Frequency cutoff in Hz\n    \n    FILTER_ORDER = 1            # Butterworth filter order\n                                # - Order 1: Gentle rolloff, minimal phase distortion\n                                # - Order 2: Steeper rolloff, good balance (recommended)\n                                # - Order 3+: Very steep rolloff, may introduce artifacts\n    \n    MIN_SAMPLES_FOR_FILTER = 10 # Minimum number of samples required to apply filtering\n                                # - Filters need sufficient data to work properly\n                                # - Increase if you get filter instability warnings\n                                # - Decrease only if working with very short sequences\n    \n    DEFAULT_SAMPLE_RATE = 50    # Assumed sample rate in Hz if timestamps unavailable\n                                # - Common IMU rates: 25, 50, 100, 200, 1000 Hz\n                                # - Should match your actual IMU sampling rate\n    \n    # Calculate time step\n    if \"timestamp\" in imu.columns:\n        imu[\"timestamp\"] = imu[\"timestamp\"].astype(float) * 1e-6  # µs → s\n        dt = np.median(imu[\"timestamp\"].diff().dropna())\n    else:\n        dt = 1 / DEFAULT_SAMPLE_RATE\n    if verbose:\n        print(f\"Δt ≈ {dt:.4f} s  (~{1/dt:.1f} Hz)\")\n    \n    # Get raw acceleration data (AC signal only)\n    acc_raw = imu[ACC_COLS].values\n    \n    # === OPTIONAL ORIENTATION PROCESSING ===\n    if use_orientation and all(col in imu.columns for col in ORI_COLS):\n        quaternions = imu[ORI_COLS].values  # [w, x, y, z]\n        quaternions = quaternions / np.linalg.norm(quaternions, axis=1, keepdims=True)  # Normalize\n        \n        # Transform to world frame and remove gravity\n        gravity_world = np.array([0, 0, 9.81])\n        acc_processed = np.zeros_like(acc_raw)\n        \n        for i in range(len(acc_raw)):\n            R = quaternion_to_rotation_matrix(quaternions[i])\n            acc_processed[i] = R.T @ acc_raw[i] - gravity_world\n        \n        if verbose:\n            print(\"Applied orientation transformation and gravity removal\")\n    else:\n        acc_processed = acc_raw\n        if verbose:\n            print(\"Using raw body-frame acceleration (no orientation processing)\")\n    \n    if verbose:\n        print(\"Acceleration statistics:\")\n        print(f\"  Mean: {acc_processed.mean(axis=0)}\")\n        print(f\"  Std:  {acc_processed.std(axis=0)}\")\n        print(f\"  Range: X=[{acc_processed[:,0].min():.2f}, {acc_processed[:,0].max():.2f}]\")\n        print(f\"         Y=[{acc_processed[:,1].min():.2f}, {acc_processed[:,1].max():.2f}]\")\n        print(f\"         Z=[{acc_processed[:,2].min():.2f}, {acc_processed[:,2].max():.2f}]\")\n    \n    # === FILTERING SECTION ===\n    if enable_filtering:\n        # High-pass filter: Remove low-frequency drift components\n        if len(acc_processed) > MIN_SAMPLES_FOR_FILTER:\n            nyquist = 0.5 / dt\n            normal_cutoff = HIGHPASS_CUTOFF_HZ / nyquist\n            \n            # Design Butterworth high-pass filter\n            b_hp, a_hp = butter(FILTER_ORDER, normal_cutoff, btype=\"high\", analog=False)\n            \n            # Apply high-pass filter to acceleration data\n            acc_filtered = np.zeros_like(acc_processed)\n            for axis in range(3):\n                acc_filtered[:, axis] = filtfilt(b_hp, a_hp, acc_processed[:, axis])\n            \n            if verbose:\n                print(f\"Applied {HIGHPASS_CUTOFF_HZ} Hz high-pass filter (order {FILTER_ORDER}) to acceleration\")\n        else:\n            acc_filtered = acc_processed\n            if verbose:\n                print(f\"Skipped filtering (only {len(acc_processed)} samples, need >{MIN_SAMPLES_FOR_FILTER})\")\n    else:\n        acc_filtered = acc_processed\n        if verbose:\n            print(\"Filtering disabled - using processed acceleration data\")\n    \n    # === INTEGRATION SECTION ===\n    # First integration: acceleration → velocity\n    vel = np.vstack((np.zeros(3), cumulative_trapezoid(acc_filtered, dx=dt, axis=0)))\n    \n    # Apply same high-pass filter to velocity if filtering is enabled\n    if enable_filtering and len(vel) > MIN_SAMPLES_FOR_FILTER:\n        vel_filtered = np.zeros_like(vel)\n        for axis in range(3):\n            vel_filtered[:, axis] = filtfilt(b_hp, a_hp, vel[:, axis])\n        vel = vel_filtered\n        \n        if verbose:\n            print(f\"Applied {HIGHPASS_CUTOFF_HZ} Hz high-pass filter to velocity\")\n    elif verbose and enable_filtering:\n        print(f\"Skipped velocity filtering (only {len(vel)} samples)\")\n    elif verbose:\n        print(\"Velocity filtering disabled\")\n    \n    # Second integration: velocity → position\n    pos = np.vstack((np.zeros(3), cumulative_trapezoid(vel, dx=dt, axis=0)))\n    \n    # Trajectory centering: Remove DC offset from position\n    pos_centered = pos - pos.mean(axis=0)\n    \n    if verbose:\n        print(\"Final position range:\")\n        print(f\"  X: {pos_centered[:,0].min():.3f} to {pos_centered[:,0].max():.3f} m\")\n        print(f\"  Y: {pos_centered[:,1].min():.3f} to {pos_centered[:,1].max():.3f} m\")\n        print(f\"  Z: {pos_centered[:,2].min():.3f} to {pos_centered[:,2].max():.3f} m\")\n    \n    # Map behaviors to integer IDs\n    behaviors = imu[\"behavior\"].astype(\"category\")\n    imu[\"beh_id\"] = behaviors.cat.codes\n    \n    return {\n        \"pos\": pos_centered,\n        \"vel\": vel,\n        \"acc_world\": acc_filtered,  # Processed acceleration (raw, or world-frame with gravity removed)\n        \"imu\": imu,\n        \"dt\": dt,\n        \"gravity_estimate\": 9.81 if use_orientation else None,\n        \"filtering_enabled\": enable_filtering,\n        \"orientation_used\": use_orientation and all(col in imu.columns for col in ORI_COLS),\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:12:15.577839Z","iopub.execute_input":"2025-06-14T07:12:15.578081Z","iopub.status.idle":"2025-06-14T07:12:15.593666Z","shell.execute_reply.started":"2025-06-14T07:12:15.578062Z","shell.execute_reply":"2025-06-14T07:12:15.592897Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Function to create animations","metadata":{}},{"cell_type":"code","source":"def create_trajectory_animation(integration_results, title=None, gesture_only=True):\n    \"\"\"\n    Create a memory-efficient animated 3D trajectory plot from IMU integration results.\n    \n    Args:\n        integration_results: dict from integrate_imu_sequence()\n        gesture_only: if True, only plot data where behavior == \"Performs gesture\"\n    \n    Returns:\n        plotly Figure object\n    \"\"\"\n    pos = integration_results['pos']\n    imu = integration_results['imu']\n    \n    # Filter to gesture behaviors only\n    if gesture_only and 'behavior' in imu.columns:\n        gesture_mask = imu['behavior'] == \"Performs gesture\"\n        if gesture_mask.any():\n            pos = pos[gesture_mask]\n            imu = imu[gesture_mask].reset_index(drop=True)\n    \n    # Get quaternion data (keeping for potential future use)\n    q_wxyz = imu[ORI_COLS].values\n    q_xyzw = q_wxyz[:, [1, 2, 3, 0]]  # wxyz → xyzw\n    rotations = R.from_quat(q_xyzw)\n    \n    # Calculate axis ranges with minimal padding\n    mins, maxs = pos.min(axis=0), pos.max(axis=0)\n    span = maxs - mins\n    pad = 0.05 * span + 1e-3\n    x_rng = [mins[0]-pad[0], maxs[0]+pad[0]]\n    y_rng = [mins[1]-pad[1], maxs[1]+pad[1]]\n    z_rng = [mins[2]-pad[2], maxs[2]+pad[2]]\n    \n    # Set up camera\n    camera = dict(up=dict(x=0, y=0, z=1), center=dict(x=0, y=0, z=0), eye=dict(x=1.25, y=1.25, z=0.8))\n    \n    # Color setup - \"Performs gesture\" always gets blue (ID 0)\n    behaviors = imu[\"behavior\"].astype(\"category\")\n    unique_behaviors = list(behaviors.cat.categories)\n    \n    # Create behavior-to-ID mapping with \"Performs gesture\" always ID 0 (blue)\n    behavior_to_id = {}\n    \n    # Always assign \"Performs gesture\" as ID 0 (blue)\n    if \"Performs gesture\" in unique_behaviors:\n        behavior_to_id[\"Performs gesture\"] = 0\n    \n    # Get non-gesture behaviors in order of first appearance\n    non_gesture_behaviors = []\n    seen_behaviors = set()\n    \n    for behavior in imu[\"behavior\"]:\n        if behavior != \"Performs gesture\" and behavior not in seen_behaviors:\n            non_gesture_behaviors.append(behavior)\n            seen_behaviors.add(behavior)\n    \n    # Assign IDs to non-gesture behaviors starting from 1\n    for i, behavior in enumerate(non_gesture_behaviors):\n        behavior_to_id[behavior] = i + 1\n    \n    # Map behaviors to IDs\n    beh_ids = behaviors.map(behavior_to_id).values\n    \n    # Convert to numpy array to avoid categorical issues\n    beh_ids = np.array(beh_ids)\n    \n    # Define colors: blue for gesture, red for first non-gesture, green for second non-gesture\n    behavior_colors = [\"#1f77b4\", \"#ff0000\", \"#00ff00\"]  # blue, red, green\n    # Add more colors if needed\n    while len(behavior_colors) < len(unique_behaviors):\n        behavior_colors.extend([\"#ff8c00\", \"#8a2be2\", \"#00ffff\"])  # orange, purple, cyan\n    \n    # Build colorscale\n    colorscale = []\n    num_behaviors = len(unique_behaviors)\n    for i in range(num_behaviors):\n        color = behavior_colors[i]\n        if num_behaviors == 1:\n            colorscale = [[0, color], [1, color]]\n        else:\n            frac_start = i / num_behaviors\n            frac_end = (i + 1) / num_behaviors\n            colorscale.extend([[frac_start, color], [frac_end, color]])\n    \n    # MEMORY OPTIMIZATION: Reduce number of frames significantly\n    # Use fewer frames but with smoother transitions\n    max_frames = min(150, len(pos))  # Cap at 150 frames maximum\n    frame_indices = np.linspace(0, len(pos)-1, max_frames, dtype=int)\n    \n    def create_frame_data(end_idx):\n        \"\"\"Create data for frame ending at end_idx\"\"\"\n        # Only store what's needed for this frame\n        return [\n            go.Scatter3d(\n                x=pos[:end_idx+1, 0], \n                y=pos[:end_idx+1, 1], \n                z=pos[:end_idx+1, 2],\n                mode=\"lines\",\n                line=dict(\n                    width=4, \n                    color=beh_ids[:end_idx+1], \n                    colorscale=colorscale, \n                    cmin=0, \n                    cmax=num_behaviors-1\n                ),\n                showlegend=False,\n                # Memory optimization: don't store unnecessary properties\n                hoverinfo='skip'\n            )\n        ]\n    \n    # Create frames with reduced memory footprint\n    frames = []\n    for idx in frame_indices:\n        frame = go.Frame(\n            data=create_frame_data(idx),\n            layout=dict(scene=dict(\n                xaxis=dict(range=x_rng, autorange=False),\n                yaxis=dict(range=y_rng, autorange=False), \n                zaxis=dict(range=z_rng, autorange=False),\n                aspectmode=\"cube\"\n            )),\n            # Memory optimization: don't store frame names\n            name=None\n        )\n        frames.append(frame)\n    \n    # Initial figure - show complete trajectory\n    initial_data = [go.Scatter3d(\n        x=pos[:, 0], y=pos[:, 1], z=pos[:, 2],\n        mode=\"lines\",\n        line=dict(width=4, color=beh_ids, colorscale=colorscale, cmin=0, cmax=num_behaviors-1),\n        showlegend=False,\n        hoverinfo='skip'  # Disable hover to save memory\n    )]\n    \n    fig = go.Figure(\n        data=initial_data,\n        frames=frames,\n        layout=go.Layout(\n            width=900, height=700, autosize=False,\n            margin=dict(l=0, r=0, t=0, b=0),\n            showlegend=False,\n            scene=dict(\n                xaxis=dict(range=x_rng, autorange=False, showticklabels=False, title=\"\", \n                          showgrid=True, gridcolor=\"lightgray\", gridwidth=1),\n                yaxis=dict(range=y_rng, autorange=False, showticklabels=False, title=\"\",\n                          showgrid=True, gridcolor=\"lightgray\", gridwidth=1),\n                zaxis=dict(range=z_rng, autorange=False, showticklabels=False, title=\"\",\n                          showgrid=True, gridcolor=\"lightgray\", gridwidth=1),\n                aspectmode=\"cube\", camera=camera\n            ),\n            dragmode=\"orbit\",\n            updatemenus=[dict(\n                type=\"buttons\",\n                buttons=[\n                    dict(label=\"Play\", method=\"animate\", \n                         args=[None, {\"frame\": {\"duration\": 50, \"redraw\": True}, \"transition\": {\"duration\": 20}}]),\n                    dict(label=\"Pause\", method=\"animate\", \n                         args=[[None], {\"frame\": {\"duration\": 0, \"redraw\": False}}])\n                ],\n                showactive=False, x=0.02, y=0.98\n            )]\n        )\n    )\n    \n    return fig","metadata":{"execution":{"iopub.status.busy":"2025-06-14T07:12:15.594369Z","iopub.execute_input":"2025-06-14T07:12:15.594580Z","iopub.status.idle":"2025-06-14T07:12:15.614807Z","shell.execute_reply.started":"2025-06-14T07:12:15.594566Z","shell.execute_reply":"2025-06-14T07:12:15.614114Z"},"trusted":true,"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Review Gestures / Orientations","metadata":{}},{"cell_type":"code","source":"# Count samples by gesture and orientation\ncounts = raw.groupby(['gesture', 'orientation']).size().reset_index(name='samples')\n\n# Get unique lists for numbering\nunique_gestures = sorted(counts['gesture'].unique())\nunique_orientations = sorted(counts['orientation'].unique())\n\nprint(\"=== GESTURE-ORIENTATION SAMPLE COUNTS ===\")\n\n# List orientations once\nprint(\"\\nORIENTATIONS:\")\nfor i, orientation in enumerate(unique_orientations, 1):\n    print(f\"  {i}. {orientation}\")\n\nprint(\"\\nGESTURES (AVAIL. ORIENTATIONS):\")\nfor i, gesture in enumerate(unique_gestures, 1):\n    # Get which orientations are present for this gesture\n    present_orientations = []\n    for ori_num, orientation in enumerate(unique_orientations, 1):\n        gesture_data = counts[(counts['gesture'] == gesture) & (counts['orientation'] == orientation)]\n        if len(gesture_data) > 0:  # Only include if there are samples\n            present_orientations.append(str(ori_num))\n    \n    print(f\"  {gesture}: {', '.join(present_orientations)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:12:15.616412Z","iopub.execute_input":"2025-06-14T07:12:15.616663Z","iopub.status.idle":"2025-06-14T07:12:15.722218Z","shell.execute_reply.started":"2025-06-14T07:12:15.616645Z","shell.execute_reply":"2025-06-14T07:12:15.721575Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Function that visualizes for a given gesture / orientation combination","metadata":{}},{"cell_type":"code","source":"def visualize_by_index(gesture_name, orientation=None, samples=1, gesture_only=True):\n    \"\"\"\n    Visualize sequences by gesture and orientation indices.\n        \"\"\"    \n    # Filter by gesture, and optionally by orientation\n    filter_func = lambda x: x['gesture'].iloc[0] == gesture_name\n    if orientation is not None:\n        orientation_name = unique_orientations[orientation - 1]\n        filter_func = lambda x: x['gesture'].iloc[0] == gesture_name and x['orientation'].iloc[0] == orientation_name\n    \n    matches = raw.groupby('sequence_id').filter(filter_func)\n    sequence_ids = matches['sequence_id'].unique()\n    \n    if len(sequence_ids) == 0:\n        print(\"❌ No matching sequences found\")\n        return []\n    \n    # If no specific orientation requested, pick the first available orientation\n    if orientation is None:\n        # Find the first available orientation for this gesture\n        first_seq_id = sequence_ids[0]\n        chosen_orientation = raw[raw.sequence_id == first_seq_id]['orientation'].iloc[0]\n        \n        # Filter to only sequences with this orientation\n        filter_func = lambda x: x['gesture'].iloc[0] == gesture_name and x['orientation'].iloc[0] == chosen_orientation\n        matches = raw.groupby('sequence_id').filter(filter_func)\n        sequence_ids = matches['sequence_id'].unique()\n        search_desc = f\"{gesture_name} + {chosen_orientation}\"\n    else:\n        search_desc = f\"{gesture_name} + {orientation_name}\"\n    \n    # Take the requested number of samples\n    selected_sequences = sequence_ids[:samples]\n    \n    # Display header\n    gesture_filter = \"Gestures Only\" if gesture_only else \"Sequence Includes Transitions\"\n    print(f\"\\n{'='*80}\")\n    print(f\"🎯 {search_desc.upper()}\")\n    print(f\"📊 Showing {len(selected_sequences)} sequences ({gesture_filter})\")\n    print(f\"{'='*80}\")\n        \n    figures = []\n    for i, seq_id in enumerate(selected_sequences, 1):\n        imu = raw[raw.sequence_id == seq_id].copy().reset_index(drop=True)\n        results = integrate_imu_sequence(imu)\n        actual_orientation = imu['orientation'].iloc[0]\n        title = f\"{gesture_name} - {actual_orientation} (#{i}, ID:{seq_id})\"\n        fig = create_trajectory_animation(results, title, gesture_only=gesture_only)\n        figures.append(fig)\n        fig.show(config=dict(responsive=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:12:15.722873Z","iopub.execute_input":"2025-06-14T07:12:15.723081Z","iopub.status.idle":"2025-06-14T07:12:15.731562Z","shell.execute_reply.started":"2025-06-14T07:12:15.723063Z","shell.execute_reply":"2025-06-14T07:12:15.730631Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# \"Glasses on/off\"","metadata":{}},{"cell_type":"code","source":"visualize_by_index(\"Glasses on/off\", samples=3, gesture_only=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:16:52.287439Z","iopub.execute_input":"2025-06-14T07:16:52.288073Z","iopub.status.idle":"2025-06-14T07:16:54.182802Z","shell.execute_reply.started":"2025-06-14T07:16:52.288050Z","shell.execute_reply":"2025-06-14T07:16:54.181860Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# \"Write name in air\" (just the gesture)\n* Can't quite read anyones name - but we are only looking at a few samples...","metadata":{}},{"cell_type":"code","source":"visualize_by_index(\"Write name in air\", orientation=2, samples=3, gesture_only=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:12:17.148840Z","iopub.execute_input":"2025-06-14T07:12:17.149008Z","iopub.status.idle":"2025-06-14T07:12:18.116578Z","shell.execute_reply.started":"2025-06-14T07:12:17.148995Z","shell.execute_reply":"2025-06-14T07:12:18.115803Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# \"Forehead - pull hairline\"","metadata":{}},{"cell_type":"code","source":"visualize_by_index(\"Forehead - pull hairline\", samples=3, gesture_only=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:20:53.738278Z","iopub.execute_input":"2025-06-14T07:20:53.738666Z","iopub.status.idle":"2025-06-14T07:20:55.580520Z","shell.execute_reply.started":"2025-06-14T07:20:53.738635Z","shell.execute_reply":"2025-06-14T07:20:55.579532Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# \"Wave hello\"","metadata":{}},{"cell_type":"code","source":"visualize_by_index(\"Wave hello\", samples=3, gesture_only=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-14T07:26:12.624352Z","iopub.execute_input":"2025-06-14T07:26:12.624702Z","iopub.status.idle":"2025-06-14T07:26:14.645158Z","shell.execute_reply.started":"2025-06-14T07:26:12.624685Z","shell.execute_reply":"2025-06-14T07:26:14.644209Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# \"Pull air toward your face\"","metadata":{}},{"cell_type":"code","source":"visualize_by_index(\"Pull air toward your face\", samples=3, gesture_only=False)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}