#!/usr/bin/env python3 """ Dataset Augmentation Script for A15 Cut Skeleton Sequences Applies the following augmentations to each cut sequence CSV in a15_cut/: 1. Mirror on y-axis (flip x-coordinates left ↔ right) 2. Rotate on y-axis by ±10 degrees 3. Stretch/compress a few % in x, y, z axes Each augmented exercise retains the same quality score as the original. The cut CSVs have columns: FrameNo, head_x, head_y, head_z, ... (13 joints × 3 coords) Each row is one frame; the full file is one exercise sequence. Output: - a15_cut_augmented/ directory with all original + augmented CSV files - a15_augmented_data.csv combined CSV with clip names, scores, probabilities Usage: python3 augment_cut_data.py """ import os import glob import pandas as pd import numpy as np from typing import List, Tuple # ── coordinate helpers ────────────────────────────────────────────────────────── def get_coordinate_columns(df: pd.DataFrame) -> Tuple[List[int], List[int], List[int]]: """ After FrameNo (column 0), the columns follow the pattern joint1_x, joint1_y, joint1_z, joint2_x, joint2_y, joint2_z, … so (col_index - 1) % 3 tells us the axis. Returns: (x_indices, y_indices, z_indices) — column indices (0-based) into the DataFrame. """ coord_cols = list(range(1, len(df.columns))) # skip FrameNo x_idx = [c for c in coord_cols if (c - 1) % 3 == 0] y_idx = [c for c in coord_cols if (c - 1) % 3 == 1] z_idx = [c for c in coord_cols if (c - 1) % 3 == 2] return x_idx, y_idx, z_idx # ── augmentation primitives ──────────────────────────────────────────────────── def mirror_on_y_axis(df: pd.DataFrame, x_idx: List[int]) -> pd.DataFrame: """Mirror left ↔ right by negating all x-coordinates.""" df_aug = df.copy() for idx in x_idx: df_aug.iloc[:, idx] = -df.iloc[:, idx] return df_aug def rotate_on_y_axis(df: pd.DataFrame, x_idx: List[int], z_idx: List[int], angle_deg: float) -> pd.DataFrame: """ Rotate every joint around the y-axis. x' = x·cos(θ) + z·sin(θ) y' = y z' = –x·sin(θ) + z·cos(θ) """ df_aug = df.copy() angle_rad = np.radians(angle_deg) cos_a = np.cos(angle_rad) sin_a = np.sin(angle_rad) for xi, zi in zip(x_idx, z_idx): x_orig = df.iloc[:, xi].values z_orig = df.iloc[:, zi].values df_aug.iloc[:, xi] = x_orig * cos_a + z_orig * sin_a df_aug.iloc[:, zi] = -x_orig * sin_a + z_orig * cos_a return df_aug def stretch_compress(df: pd.DataFrame, x_idx: List[int], y_idx: List[int], z_idx: List[int], scale_x: float, scale_y: float, scale_z: float) -> pd.DataFrame: """Scale coordinates independently per axis.""" df_aug = df.copy() for idx in x_idx: df_aug.iloc[:, idx] *= scale_x for idx in y_idx: df_aug.iloc[:, idx] *= scale_y for idx in z_idx: df_aug.iloc[:, idx] *= scale_z return df_aug # ── I/O helpers ───────────────────────────────────────────────────────────────── def load_score_map(score_file: str) -> dict: """Return {clip_name: (score_rescaled, good_probability)} from a15_good_rescaled.csv.""" df_scores = pd.read_csv(score_file) return { row['clip']: (row['score_rescaled'], row['good_probability']) for _, row in df_scores.iterrows() } def augment_all_sequences(input_dir: str, score_file: str, output_dir: str, output_csv: str) -> None: # Load scores print(f"Loading scores from {score_file} …") score_map = load_score_map(score_file) print(f" → {len(score_map)} clips with scores") # Discover cut sequence CSVs csv_pattern = os.path.join(input_dir, '*_kinect.csv') csv_files = sorted(glob.glob(csv_pattern)) n_original = len(csv_files) if n_original == 0: print(f"ERROR: No *_kinect.csv files found in {input_dir}.") return print(f"Found {n_original} original sequence files") # Create output directory os.makedirs(output_dir, exist_ok=True) # Augmentation recipes → (suffix, callable) augmentations = [ ('mirror', lambda df, x, y, z: mirror_on_y_axis(df, x)), ('rotate_pos', lambda df, x, y, z: rotate_on_y_axis(df, x, z, 10)), ('rotate_neg', lambda df, x, y, z: rotate_on_y_axis(df, x, z, -10)), ('stretch', lambda df, x, y, z: stretch_compress(df, x, y, z, 1.05, 0.95, 1.02)), ] all_entries: List[Tuple[str, float, float]] = [] # (clip, score, prob) total_augmented = 0 for csv_path in csv_files: basename = os.path.basename(csv_path) # e.g. "A100_kinect.csv" clip_name = basename.replace('.csv', '') # e.g. "A100_kinect" # Skip if no score is known (should not happen for the good list) if clip_name not in score_map: print(f" ⚠ WARNING: no score for {clip_name}, skipping") continue score_val, prob_val = score_map[clip_name] # Read the frame-by-frame skeleton data df = pd.read_csv(csv_path) x_idx, y_idx, z_idx = get_coordinate_columns(df) # ── 1. Keep original ── orig_path = os.path.join(output_dir, basename) df.to_csv(orig_path, index=False) all_entries.append((clip_name, score_val, prob_val)) # ── 2–5. Augmented variants ── for suffix, aug_fn in augmentations: df_aug = aug_fn(df.copy(), x_idx, y_idx, z_idx) aug_clip_name = f"{clip_name}_{suffix}" aug_filename = f"{aug_clip_name}.csv" df_aug.to_csv(os.path.join(output_dir, aug_filename), index=False) all_entries.append((aug_clip_name, score_val, prob_val)) total_augmented += 1 if (len(csv_files) <= 10 or csv_files.index(csv_path) % 20 == 0): print(f" ✓ {clip_name} → original + 4 augmented variants") # ── Write combined metadata CSV ── df_out = pd.DataFrame(all_entries, columns=['clip', 'score_rescaled', 'good_probability']) df_out.to_csv(output_csv, index=False) print(f"\n{'=' * 50}") print(f" Augmentation complete") print(f" Original clips : {n_original}") print(f" Augmented variants : {total_augmented}") print(f" Total (orig + augment) : {len(df_out)}") print(f" Augmented CSV directory : {output_dir}/") print(f" Combined metadata CSV : {output_csv}") print(f"{'=' * 50}") # ── entry point ───────────────────────────────────────────────────────────────── def main(): # Paths relative to this script's directory script_dir = os.path.dirname(os.path.abspath(__file__)) input_dir = os.path.join(script_dir, 'a15_cut') score_file = os.path.join(script_dir, 'a15_good_rescaled.csv') output_dir = os.path.join(script_dir, 'a15_cut_augmented') output_csv = os.path.join(script_dir, 'a15_augmented_data.csv') augment_all_sequences( input_dir=input_dir, score_file=score_file, output_dir=output_dir, output_csv=output_csv, ) if __name__ == '__main__': main()