{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":99552,"databundleVersionId":13190393,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Rotation Augmentation\n\nClever augmentations are important to generalizing a detector / classifier. Here I implement rotation augmentation:\n- I rotate the image by a given angle\n- I transform the bbox label to move to the specified point","metadata":{}},{"cell_type":"code","source":"import os\nimport json\nimport glob\nimport warnings\nimport pydicom\nimport seaborn as sns\nimport numpy as np\nimport pandas as pd\nimport nibabel as nib\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom random import sample\nfrom scipy.ndimage import zoom\n\nwarnings.filterwarnings(\"ignore\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-05T15:42:25.251569Z","iopub.execute_input":"2025-08-05T15:42:25.251806Z","iopub.status.idle":"2025-08-05T15:42:28.9366Z","shell.execute_reply.started":"2025-08-05T15:42:25.251785Z","shell.execute_reply":"2025-08-05T15:42:28.9357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define paths\ntrain_csv_path = '/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv'     \nlocalizers_csv_path = '/kaggle/input/rsna-intracranial-aneurysm-detection/train_localizers.csv'\nseries_dir = '/kaggle/input/rsna-intracranial-aneurysm-detection/series'                \n\n# Load train.csv as df\ndf = pd.read_csv(train_csv_path)\n# Load train_localizers.csv as loc_df\nloc_df = pd.read_csv(localizers_csv_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-05T15:42:28.937643Z","iopub.execute_input":"2025-08-05T15:42:28.938189Z","iopub.status.idle":"2025-08-05T15:42:28.999065Z","shell.execute_reply.started":"2025-08-05T15:42:28.938159Z","shell.execute_reply":"2025-08-05T15:42:28.998031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nfrom scipy.ndimage import rotate\n\nCMAP = 'gray'\n\ndef rotate_image_and_coordinates(img, x, y, angle_degrees):\n    \"\"\"\n    Rotate image and adjust coordinates accordingly.\n    \n    Args:\n        img: 2D numpy array (image)\n        x, y: original coordinates\n        angle_degrees: rotation angle in degrees (positive = counterclockwise)\n    \n    Returns:\n        rotated_img: rotated image\n        new_x, new_y: adjusted coordinates\n    \"\"\"\n    height, width = img.shape\n    \n    # Find the background value (minimum value in the image, or corners)\n    # Using corners to determine background value\n    corner_values = [img[0, 0], img[0, -1], img[-1, 0], img[-1, -1]]\n    background_value = min(corner_values)\n    # Alternative: use minimum value in entire image\n    # background_value = img.min()\n    \n    # Rotate image using scipy.ndimage.rotate with proper background value\n    rotated_img = rotate(img, angle_degrees, reshape=False, mode='constant', cval=background_value)\n    \n    # For coordinate transformation, we need to use the same rotation as scipy\n    # scipy.ndimage.rotate rotates counterclockwise, so we use the same angle\n    angle_rad = np.radians(-angle_degrees)  # Note: negative because of coordinate system\n    cos_angle = np.cos(angle_rad)\n    sin_angle = np.sin(angle_rad)\n    \n    # Center of rotation (image center)\n    center_x = (width - 1) / 2.0\n    center_y = (height - 1) / 2.0\n    \n    # Translate coordinates to origin (center of image)\n    x_centered = x - center_x\n    y_centered = y - center_y\n    \n    # Apply rotation transformation\n    new_x_centered = x_centered * cos_angle - y_centered * sin_angle\n    new_y_centered = x_centered * sin_angle + y_centered * cos_angle\n    \n    # Translate back to image coordinates\n    new_x = new_x_centered + center_x\n    new_y = new_y_centered + center_y\n    \n    # Clamp coordinates to image bounds\n    new_x = np.clip(new_x, 0, width - 1)\n    new_y = np.clip(new_y, 0, height - 1)\n    \n    return rotated_img, new_x, new_y\n\ndef plot_labeled_slice(row_number, localizers_df, train_df, ax_image, ax_label, ax_overlay, rotation_angle=0):\n    row = localizers_df.iloc[row_number]\n    series_uid = row['SeriesInstanceUID']\n    sop_uid = row['SOPInstanceUID']\n    coordinates = row['coordinates']\n    location = row['location']\n    \n    # Get Modality from train_df\n    modality = train_df[train_df['SeriesInstanceUID'] == series_uid]['Modality'].iloc[0] if not train_df[train_df['SeriesInstanceUID'] == series_uid].empty else 'Unknown'\n    \n    # Find the DICOM file for the SOPInstanceUID\n    folders = glob.glob(\"../input/rsna-intracranial-aneurysm-detection/series/*\")\n    series_path = None\n    for folder in folders:\n        if series_uid in folder:\n            series_path = folder\n            break\n    \n    if not series_path:\n        print(f\"No folder found for SeriesInstanceUID: {series_uid}\")\n        return\n    \n    # Get all DICOM files in the series\n    files = sorted(glob.glob(os.path.join(series_path, \"*.dcm\")))\n    dicom_file = None\n    for file in files:\n        if sop_uid in file:\n            dicom_file = file\n            break\n    \n    if not dicom_file:\n        print(f\"No DICOM file found for SOPInstanceUID: {sop_uid}\")\n        return\n    \n    # Load DICOM image\n    ex = pydicom.dcmread(dicom_file)\n    if len(ex.pixel_array.shape) > 2:\n        print(f\"Skipping {sop_uid}: Image has more than 2 dimensions\")\n        return\n    \n    img = ex.pixel_array\n    img = np.flipud(img)  # Flip vertically to match DICOM top-left origin\n    height, width = img.shape\n    \n    # Parse coordinates\n    coordinates = json.loads(coordinates.replace(\"'\", '\"'))\n    x = coordinates[\"x\"]\n    y = height - 1 - coordinates[\"y\"]  # Adjust for flipud\n    \n    # Apply rotation if specified\n    if rotation_angle != 0:\n        img, x, y = rotate_image_and_coordinates(img, x, y, rotation_angle)\n    \n    # Plot Image\n    ax_image.imshow(img, cmap=CMAP, origin='upper', aspect=1)\n    title = f'Image (Rotated {rotation_angle}°)\\nSeries: {series_uid[:8]}...' if rotation_angle != 0 else f'Image\\nSeries: {series_uid[:8]}...'\n    ax_image.set_title(title)\n    ax_image.axis('off')\n    \n    # Plot Label (black background with red rectangle and text)\n    ax_label.imshow(np.zeros_like(img), cmap=CMAP, origin='upper', aspect=1)\n    rect = patches.Rectangle((x-10, y-10), 20, 20, linewidth=1, edgecolor='r', facecolor='none')\n    ax_label.add_patch(rect)\n    ax_label.text(x, y+35, location, color='yellow', fontsize=8, ha='center', va='bottom')\n    ax_label.text(x, y+55, f'Modality: {modality}', color='red', fontsize=8, ha='center', va='bottom')\n    ax_label.set_title('Label')\n    ax_label.axis('off')\n    \n    # Plot Overlay\n    ax_overlay.imshow(img, cmap=CMAP, origin='upper', aspect=1)\n    rect = patches.Rectangle((x-10, y-10), 20, 20, linewidth=1, edgecolor='r', facecolor='none')\n    ax_overlay.add_patch(rect)\n    ax_overlay.text(x, y+35, location, color='yellow', fontsize=8, ha='center', va='bottom')\n    ax_overlay.text(x, y+55, f'Modality: {modality}', color='red', fontsize=8, ha='center', va='bottom')\n    ax_overlay.set_title('Overlay')\n    ax_overlay.axis('off')\n\n# Function to plot for a given modality with rotation augmentation\ndef plot_modality_slices(modality, label=None, row_indices=None, rotation_angles=None):\n    \"\"\"\n    Plot modality slices with optional rotation augmentation.\n    \n    Args:\n        modality: Image modality (e.g., 'CTA', 'MRA')\n        label: Optional location filter\n        row_indices: Optional specific row indices to use\n        rotation_angles: List of rotation angles to apply (one per row). If None, no rotation.\n    \"\"\"\n    # Filter for series with Aneurysm Present and localization data\n    df_aneurysm = df[df['Aneurysm Present'] == 1]\n    loc_series = loc_df['SeriesInstanceUID'].unique()\n    df_aneurysm_loc = df_aneurysm[df_aneurysm['SeriesInstanceUID'].isin(loc_series)]\n    df_modality = df_aneurysm_loc[df_aneurysm_loc['Modality'] == modality]\n    \n    # Get sorted unique SeriesInstanceUID for the modality\n    modality_series_sorted = sorted(df_modality['SeriesInstanceUID'].unique())\n    \n    # Select rows from loc_df for the modality\n    modality_loc_rows = loc_df[loc_df['SeriesInstanceUID'].isin(modality_series_sorted)]\n    \n    # Optionally filter by label (aneurysm location)\n    if label is not None:\n        modality_loc_rows = modality_loc_rows[modality_loc_rows['location'] == label]\n    \n    # If specific row indices are provided, try to use them\n    selected_rows = []\n    if row_indices is not None:\n        for idx in row_indices:\n            if idx < len(modality_loc_rows) and modality_loc_rows.iloc[idx]['SeriesInstanceUID'] in modality_series_sorted:\n                selected_rows.append(modality_loc_rows.index[idx])\n    \n    # If fewer than 5 rows selected (or none), fall back to first 5 sorted series\n    if len(selected_rows) < 5:\n        if row_indices is not None:\n            print(f\"Requested indices {row_indices} invalid or insufficient. Selecting first {5 - len(selected_rows)} sorted series.\")\n        remaining_series = [s for s in modality_series_sorted if s not in [loc_df.iloc[idx]['SeriesInstanceUID'] for idx in selected_rows]]\n        for series in remaining_series[:5 - len(selected_rows)]:\n            series_rows = modality_loc_rows[modality_loc_rows['SeriesInstanceUID'] == series]\n            if not series_rows.empty:\n                selected_rows.append(series_rows.index[0])\n    \n    # If fewer than 5 series found, warn\n    if len(selected_rows) < 5:\n        print(f\"Only {len(selected_rows)} {modality} series with aneurysms and localization data found (label: {label}).\")\n    \n    # Set default rotation angles if not provided\n    if rotation_angles is None:\n        rotation_angles = [0] * len(selected_rows)\n    elif len(rotation_angles) < len(selected_rows):\n        # Extend rotation_angles to match selected_rows length\n        rotation_angles.extend([0] * (len(selected_rows) - len(rotation_angles)))\n    \n    # Create subplot grid\n    fig, axs = plt.subplots(len(selected_rows), 3, figsize=(15, 5 * len(selected_rows)))\n    \n    # Handle case of single row\n    if len(selected_rows) == 1:\n        axs = [axs]\n    \n    # Plot for each selected row with rotation\n    for i, row_number in enumerate(selected_rows):\n        rotation_angle = rotation_angles[i]\n        print(f\"\\nDisplaying labeled slice for series {loc_df.iloc[row_number]['SeriesInstanceUID'][:8]}... (row {row_number}) with rotation: {rotation_angle}°\")\n        plot_labeled_slice(row_number, loc_df, df, axs[i][0], axs[i][1], axs[i][2], rotation_angle)\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-05T15:53:44.524849Z","iopub.execute_input":"2025-08-05T15:53:44.525183Z","iopub.status.idle":"2025-08-05T15:53:44.550709Z","shell.execute_reply.started":"2025-08-05T15:53:44.525161Z","shell.execute_reply":"2025-08-05T15:53:44.54999Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Plot the same image with different rotations\nplot_modality_slices(modality='CTA', label='Left Middle Cerebral Artery', row_indices=[0, 0, 0, 0, 0], rotation_angles=[0, 15, 30, 45, -15])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-05T15:53:44.915817Z","iopub.execute_input":"2025-08-05T15:53:44.916293Z","iopub.status.idle":"2025-08-05T15:53:47.423078Z","shell.execute_reply.started":"2025-08-05T15:53:44.916262Z","shell.execute_reply":"2025-08-05T15:53:47.422005Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}