{"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":"gpu","dataSources":[{"sourceId":10338,"databundleVersionId":862042,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install ultralytics","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T08:48:48.621945Z","iopub.execute_input":"2025-05-27T08:48:48.622549Z","iopub.status.idle":"2025-05-27T08:50:03.004324Z","shell.execute_reply.started":"2025-05-27T08:48:48.62252Z","shell.execute_reply":"2025-05-27T08:50:03.003605Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install grad-cam","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T08:51:44.710763Z","iopub.execute_input":"2025-05-27T08:51:44.711079Z","iopub.status.idle":"2025-05-27T08:51:53.736026Z","shell.execute_reply.started":"2025-05-27T08:51:44.711038Z","shell.execute_reply":"2025-05-27T08:51:53.735116Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n# KAGGLE NOTEBOOK: Interpretable YOLOv8 for TB Detection & Localization\n# Author: SSEBABI SAM (Conceptual Outline by AI)\n# Based on Research Proposal: \"AN INTERPRETABLE DEEP LEARNING MODEL FOR TUBERCULOSIS \n# DETECTION AND LOCALISATION IN CHEST X-RAY IMAGES WITH YOLOv8 TECHNOLOGY\"\n# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n#--------------------------------------------------------------------------\n# 1. SETUP AND IMPORTS\n#--------------------------------------------------------------------------\nimport os\nimport shutil\nimport random\nimport yaml # For data.yaml\nimport glob # For finding files\nimport pandas as pd\nimport numpy as np\nimport cv2 # OpenCV for image processing\nimport pydicom # For reading DICOM files (common in medical imaging)\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\n\n# YOLOv8 / Ultralytics\nfrom ultralytics import YOLO\nimport torch # PyTorch\n\n# Interpretability\nfrom pytorch_grad_cam import GradCAM, HiResCAM, ScoreCAM, GradCAMPlusPlus, AblationCAM, XGradCAM, EigenCAM, FullGrad\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget, FasterRCNNBoxScoreTarget # We'll adapt for YOLO\nfrom pytorch_grad_cam.utils.image import show_cam_on_image, preprocess_image\n\n# Scikit-learn for splitting\nfrom sklearn.model_selection import train_test_split\n\n# Ensure GPU is available and set as device\nDEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {DEVICE}\")\nif DEVICE.type == 'cpu':\n    print(\"WARNING: GPU not found, training will be very slow. Ensure GPU is enabled in Kaggle settings.\")\nelse:\n    print(f\"GPU Name: {torch.cuda.get_device_name(0)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T08:52:06.187502Z","iopub.execute_input":"2025-05-27T08:52:06.188481Z","iopub.status.idle":"2025-05-27T08:52:09.610247Z","shell.execute_reply.started":"2025-05-27T08:52:06.188437Z","shell.execute_reply":"2025-05-27T08:52:09.609557Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Constants - ADAPT THESE\n# !! IMPORTANT !! For your actual research, you need a dataset with TB annotations.\n# We are using RSNA Pneumonia dataset as a STAND-IN to demonstrate the YOLO pipeline.\n# Pretend 'Pneumonia' is 'Tuberculosis' for this example.\nKAGGLE_DATASET_PATH = '/kaggle/input/rsna-pneumonia-detection-challenge' # Example dataset\nOUTPUT_DIR = '/kaggle/working/tuberculosis_detection_yolov8'\nPROCESSED_DATA_DIR = os.path.join(OUTPUT_DIR, 'datasets', 'tb_data')\nCLASS_NAME = 'tuberculosis' # Or 'TB'\nCLASS_ID = 0 # YOLO class IDs are 0-indexed\n\n# Create output directories\nos.makedirs(OUTPUT_DIR, exist_ok=True)\nos.makedirs(PROCESSED_DATA_DIR, exist_ok=True)\nos.makedirs(os.path.join(PROCESSED_DATA_DIR, 'images', 'train'), exist_ok=True)\nos.makedirs(os.path.join(PROCESSED_DATA_DIR, 'images', 'val'), exist_ok=True)\nos.makedirs(os.path.join(PROCESSED_DATA_DIR, 'labels', 'train'), exist_ok=True)\nos.makedirs(os.path.join(PROCESSED_DATA_DIR, 'labels', 'val'), exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T08:53:16.489085Z","iopub.execute_input":"2025-05-27T08:53:16.490004Z","iopub.status.idle":"2025-05-27T08:53:16.496006Z","shell.execute_reply.started":"2025-05-27T08:53:16.48998Z","shell.execute_reply":"2025-05-27T08:53:16.495326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#--------------------------------------------------------------------------\n# 2. DATA LOADING AND PREPROCESSING (Using RSNA Pneumonia as an example)\n#--------------------------------------------------------------------------\n# You will need to adapt this section heavily based on YOUR CHOSEN TB DATASET.\n# The goal is to have:\n# PROCESSED_DATA_DIR/images/train/img1.png, img2.png, ...\n# PROCESSED_DATA_DIR/images/val/imgA.png, imgB.png, ...\n# PROCESSED_DATA_DIR/labels/train/img1.txt, img2.txt, ...\n# PROCESSED_DATA_DIR/labels/val/imgA.txt, imgB.txt, ...\n#\n# Each .txt file contains: <class_id> <x_center_norm> <y_center_norm> <width_norm> <height_norm>\n\nprint(\"Starting Data Loading and Preprocessing...\")\n\n# Load annotations (example for RSNA)\ntrain_labels_df = pd.read_csv(os.path.join(KAGGLE_DATASET_PATH, 'stage_2_train_labels.csv'))\n# For RSNA, detailed class info might provide more context, but we simplify for YOLO\n# detailed_class_info_df = pd.read_csv(os.path.join(KAGGLE_DATASET_PATH, 'stage_2_detailed_class_info.csv'))\n\n# Filter for images with \"Pneumonia\" (which we are calling \"Tuberculosis\")\n# In RSNA, Target=1 means pneumonia\npositive_cases_df = train_labels_df[train_labels_df['Target'] == 1]\nnegative_cases_df = train_labels_df[train_labels_df['Target'] == 0].drop_duplicates(subset=['patientId'])\n\n\n# Let's take a subset for faster demonstration\n# Adjust these numbers for your actual training\nMAX_POSITIVE_SAMPLES = 2000 # More positive samples\nMAX_NEGATIVE_SAMPLES = 500  # Fewer negative samples for this example, but balance is important\n\nif len(positive_cases_df) > MAX_POSITIVE_SAMPLES:\n    positive_cases_df = positive_cases_df.sample(n=MAX_POSITIVE_SAMPLES, random_state=42)\nif len(negative_cases_df) > MAX_NEGATIVE_SAMPLES:\n     negative_cases_df = negative_cases_df.sample(n=MAX_NEGATIVE_SAMPLES, random_state=42) # Add some healthy controls\n\nall_patient_ids = list(positive_cases_df['patientId'].unique()) + \\\n                  list(negative_cases_df['patientId'].unique())\nrandom.shuffle(all_patient_ids)\n\n\n# Split patient IDs into train and validation sets\ntrain_ids, val_ids = train_test_split(all_patient_ids, test_size=0.2, random_state=42)\n\nprint(f\"Total unique patient IDs: {len(all_patient_ids)}\")\nprint(f\"Training patient IDs: {len(train_ids)}\")\nprint(f\"Validation patient IDs: {len(val_ids)}\")\n\nprocessed_image_count = 0\n\ndef process_patient(patient_id, split):\n    global processed_image_count\n    image_path_dcm = os.path.join(KAGGLE_DATASET_PATH, 'stage_2_train_images', f'{patient_id}.dcm')\n    \n    if not os.path.exists(image_path_dcm):\n        # print(f\"DCM file not found for {patient_id}\")\n        return\n\n    # Read DICOM and convert to PNG\n    # dcm_data = pydicom.read_file(image_path_dcm) - Nolonger supported\n    dcm_data = pydicom.dcmread(image_path_dcm)\n    img_array = dcm_data.pixel_array\n    img_height, img_width = img_array.shape[:2] # DICOM images are often grayscale\n\n    # Normalize pixel values if necessary (e.g., for display or certain augmentations)\n    # For YOLO, raw pixel values are fine as long as they are consistent.\n    # Convert to 3-channel if it's grayscale for some pre-trained models, though YOLO handles grayscale.\n    # However, many visualization tools and CAM expect 3-channel.\n    if len(img_array.shape) == 2 or img_array.shape[2] == 1:\n        img_array_rgb = cv2.cvtColor(img_array, cv2.COLOR_GRAY2RGB)\n    else:\n        img_array_rgb = img_array\n\n    # Save image as PNG (YOLO works well with common formats)\n    image_filename_png = f'{patient_id}.png'\n    image_save_path = os.path.join(PROCESSED_DATA_DIR, 'images', split, image_filename_png)\n    cv2.imwrite(image_save_path, img_array_rgb)\n\n    # Create YOLO label file\n    label_filename_txt = f'{patient_id}.txt'\n    label_save_path = os.path.join(PROCESSED_DATA_DIR, 'labels', split, label_filename_txt)\n    \n    annotations = []\n    patient_boxes = train_labels_df[(train_labels_df['patientId'] == patient_id) & (train_labels_df['Target'] == 1)]\n    \n    if not patient_boxes.empty:\n        for _, row in patient_boxes.iterrows():\n            if pd.notna(row['x']) and pd.notna(row['y']) and pd.notna(row['width']) and pd.notna(row['height']):\n                # Convert to YOLO format: class_id x_center_norm y_center_norm width_norm height_norm\n                x_min, y_min, box_w, box_h = float(row['x']), float(row['y']), float(row['width']), float(row['height'])\n                \n                x_center = x_min + box_w / 2\n                y_center = y_min + box_h / 2\n                \n                x_center_norm = x_center / img_width\n                y_center_norm = y_center / img_height\n                width_norm = box_w / img_width\n                height_norm = box_h / img_height\n                \n                # Clamp values to be within [0, 1]\n                x_center_norm = max(0, min(1, x_center_norm))\n                y_center_norm = max(0, min(1, y_center_norm))\n                width_norm = max(0, min(1, width_norm))\n                height_norm = max(0, min(1, height_norm))\n\n                annotations.append(f\"{CLASS_ID} {x_center_norm} {y_center_norm} {width_norm} {height_norm}\")\n    \n    # Write annotations (even if empty for negative samples, YOLO handles this)\n    with open(label_save_path, 'w') as f:\n        f.write(\"\\n\".join(annotations))\n        \n    processed_image_count += 1\n    if processed_image_count % 200 == 0:\n        print(f\"Processed {processed_image_count} images...\")\n\nprint(\"Processing training data...\")\nfor patient_id in train_ids:\n    process_patient(patient_id, 'train')\n\nprint(\"Processing validation data...\")\nfor patient_id in val_ids:\n    process_patient(patient_id, 'val')\n\nprint(f\"Finished processing. Total images processed: {processed_image_count}\")\nprint(f\"Training images: {len(glob.glob(os.path.join(PROCESSED_DATA_DIR, 'images/train/*.png')))}\")\nprint(f\"Validation images: {len(glob.glob(os.path.join(PROCESSED_DATA_DIR, 'images/val/*.png')))}\")\nprint(f\"Training labels: {len(glob.glob(os.path.join(PROCESSED_DATA_DIR, 'labels/train/*.txt')))}\")\nprint(f\"Validation labels: {len(glob.glob(os.path.join(PROCESSED_DATA_DIR, 'labels/val/*.txt')))}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T08:55:28.021398Z","iopub.execute_input":"2025-05-27T08:55:28.022211Z","iopub.status.idle":"2025-05-27T08:57:38.658979Z","shell.execute_reply.started":"2025-05-27T08:55:28.022186Z","shell.execute_reply":"2025-05-27T08:57:38.658276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#--------------------------------------------------------------------------\n# 3. CREATE data.yaml FILE FOR YOLO\n#--------------------------------------------------------------------------\ndata_yaml_content = {\n    'train': os.path.join('..', 'images', 'train'), # Relative to PROCESSED_DATA_DIR/\n    'val': os.path.join('..', 'images', 'val'),   # Relative to PROCESSED_DATA_DIR/\n    'nc': 1,  # Number of classes\n    'names': [CLASS_NAME] \n}\n\n# The paths in data.yaml should be relative to the directory where data.yaml is,\n# or absolute. Ultralytics often expects them relative to the project root if data.yaml is there.\n# For simplicity, we'll place data.yaml inside PROCESSED_DATA_DIR.\n# The 'train' and 'val' paths should point to image folders. YOLO infers label paths.\n\ndata_yaml_path = os.path.join(PROCESSED_DATA_DIR, 'data.yaml')\nwith open(data_yaml_path, 'w') as f:\n    yaml.dump(data_yaml_content, f, sort_keys=False)\n\nprint(f\"data.yaml created at: {data_yaml_path}\")\nwith open(data_yaml_path, 'r') as f:\n    print(\"Content of data.yaml:\")\n    print(f.read())\n\n# Verify a few label files (optional)\nprint(\"\\nExample label files:\")\nlabel_files_train = glob.glob(os.path.join(PROCESSED_DATA_DIR, 'labels/train/*.txt'))\nif label_files_train:\n    for i in range(min(3, len(label_files_train))):\n        with open(label_files_train[i], 'r') as f_label:\n            print(f\"Content of {os.path.basename(label_files_train[i])}: \\n{f_label.read().strip()}\")\nelse:\n    print(\"No label files found in training set. Check data processing.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T08:57:54.425051Z","iopub.execute_input":"2025-05-27T08:57:54.42535Z","iopub.status.idle":"2025-05-27T08:57:54.437353Z","shell.execute_reply.started":"2025-05-27T08:57:54.425333Z","shell.execute_reply":"2025-05-27T08:57:54.436736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#--------------------------------------------------------------------------\n# 4. YOLOv8 MODEL TRAINING\n#--------------------------------------------------------------------------\n# Choose a model: yolov8n.pt (nano), yolov8s.pt (small), yolov8m.pt (medium), etc.\n# Larger models are more accurate but slower and require more VRAM.\nMODEL_ARCH = 'yolov8s.pt' # Start with small or nano for faster iteration\n\n# Training Hyperparameters - Adjust as needed\nEPOCHS = 50 # For a real project, 100-300+ might be needed\nIMG_SIZE = 640 # Default is 640. Medical images might benefit from higher res if VRAM allows.\nBATCH_SIZE = 16 # Adjust based on GPU VRAM. (e.g., 8, 16, 32). Kaggle P100/T4 can handle 16 for yolov8s.\nPATIENCE = 20 # Early stopping patience\n\nprint(f\"\\nStarting YOLOv8 training with {MODEL_ARCH}...\")\nprint(f\"Epochs: {EPOCHS}, Image Size: {IMG_SIZE}, Batch Size: {BATCH_SIZE}\")\n\n# Load a pre-trained model\nmodel = YOLO(MODEL_ARCH) \nmodel.to(DEVICE) # Ensure model is on GPU\n\n# Start training\n# The 'project' and 'name' arguments define the output directory structure.\n# results will be in OUTPUT_DIR/yolo_training_runs/exp...\nresults = model.train(\n    data=data_yaml_path,\n    epochs=EPOCHS,\n    imgsz=IMG_SIZE,\n    batch=BATCH_SIZE,\n    patience=PATIENCE,\n    project=os.path.join(OUTPUT_DIR, 'yolo_training_runs'), # Main project folder for runs\n    name='tb_detection_experiment', # Specific experiment name\n    exist_ok=True, # Allow overwriting if experiment name exists (useful for reruns)\n    # device=DEVICE # Redundant if model.to(DEVICE) was called, but good for clarity\n    # workers=4 # Number of dataloader workers, adjust based on CPU\n    # val=True # Validation is on by default if 'val' path is in data.yaml\n)\n\nprint(\"Training completed.\")\nprint(f\"Training results saved in: {results.save_dir}\") # ultralytics >= 8.0.190\n\n# The best model is usually saved as 'best.pt' in results.save_dir/weights/\nBEST_MODEL_PATH = os.path.join(results.save_dir, 'weights', 'best.pt')\nLAST_MODEL_PATH = os.path.join(results.save_dir, 'weights', 'last.pt')\n\nif os.path.exists(BEST_MODEL_PATH):\n    print(f\"Best model saved at: {BEST_MODEL_PATH}\")\nelse:\n    print(f\"WARNING: best.pt not found. Using last.pt: {LAST_MODEL_PATH}\")\n    BEST_MODEL_PATH = LAST_MODEL_PATH # Fallback","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T08:58:28.223434Z","iopub.execute_input":"2025-05-27T08:58:28.223932Z","iopub.status.idle":"2025-05-27T09:33:46.814494Z","shell.execute_reply.started":"2025-05-27T08:58:28.22391Z","shell.execute_reply":"2025-05-27T09:33:46.81359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#--------------------------------------------------------------------------\n# 5. MODEL EVALUATION (on validation set)\n#--------------------------------------------------------------------------\nif os.path.exists(BEST_MODEL_PATH):\n    print(\"\\nEvaluating the best model on the validation set...\")\n    model_eval = YOLO(BEST_MODEL_PATH) # Load the best trained model\n    \n    metrics = model_eval.val(\n        data=data_yaml_path,\n        imgsz=IMG_SIZE,\n        batch=BATCH_SIZE,\n        # conf=0.25,  # Confidence threshold for NMS\n        # iou=0.45,   # IoU threshold for NMS\n        split='val', # Ensure it uses the validation set\n        save_json=True, # Save results to JSON for pycocotools\n        project=os.path.join(OUTPUT_DIR, 'yolo_validation_runs'),\n        name='tb_validation_experiment'\n    )\n    \n    print(\"Validation Metrics:\")\n    # metrics object contains various performance indicators\n    # e.g., metrics.box.map, metrics.box.map50, metrics.box.map75\n    # For a single class, mAP is equivalent to AP for that class\n    print(f\"  mAP50-95 (Box): {metrics.box.map:.4f}\")\n    print(f\"  mAP50 (Box): {metrics.box.map50:.4f}\")\n    # Precision and Recall might also be available depending on Ultralytics version\n    # print(f\"  Precision (Box): {metrics.box.mp:.4f}\")\n    # print(f\"  Recall (Box): {metrics.box.mr:.4f}\")\n\n    # You can find plots like confusion matrix, P-R curve etc. in the save_dir of validation.\n    print(f\"Validation plots and results saved in: {metrics.save_dir}\")\nelse:\n    print(\"Skipping evaluation as best model path not found.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T09:40:27.324422Z","iopub.execute_input":"2025-05-27T09:40:27.324749Z","iopub.status.idle":"2025-05-27T09:40:38.64496Z","shell.execute_reply.started":"2025-05-27T09:40:27.32471Z","shell.execute_reply":"2025-05-27T09:40:38.644179Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#--------------------------------------------------------------------------\n# 6. PREDICTION AND VISUALIZATION (on a few validation images)\n#--------------------------------------------------------------------------\nif os.path.exists(BEST_MODEL_PATH):\n    print(\"\\nRunning predictions on a few validation images...\")\n    model_pred = YOLO(BEST_MODEL_PATH)\n\n    val_image_files = glob.glob(os.path.join(PROCESSED_DATA_DIR, 'images', 'val', '*.png'))\n    if val_image_files:\n        sample_images = random.sample(val_image_files, min(5, len(val_image_files)))\n\n        for img_path in sample_images:\n            print(f\"Predicting on: {img_path}\")\n            pred_results = model_pred.predict(\n                source=img_path, \n                save=True, # Saves image with bboxes\n                conf=0.25, # Confidence threshold\n                project=os.path.join(OUTPUT_DIR, 'yolo_prediction_outputs'),\n                name='tb_prediction_samples',\n                exist_ok=True\n            )\n            \n            # Display the image with predictions using matplotlib\n            # The predict() method already saves images if save=True.\n            # To display inline:\n            if pred_results and pred_results[0].plot() is not None:\n                img_with_boxes = pred_results[0].plot() # Returns a BGR numpy array\n                img_with_boxes_rgb = cv2.cvtColor(img_with_boxes, cv2.COLOR_BGR2RGB)\n                \n                plt.figure(figsize=(8, 8))\n                plt.imshow(img_with_boxes_rgb)\n                plt.title(f\"Predictions for {os.path.basename(img_path)}\")\n                plt.axis('off')\n                plt.show()\n            else:\n                print(f\"No detections or error in plotting for {img_path}\")\n        print(f\"Prediction images saved in {os.path.join(OUTPUT_DIR, 'yolo_prediction_outputs', 'tb_prediction_samples')}\")\n    else:\n        print(\"No validation images found to run predictions on.\")\nelse:\n    print(\"Skipping prediction as best model path not found.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T09:41:35.978674Z","iopub.execute_input":"2025-05-27T09:41:35.979473Z","execution_failed":"2025-05-27T10:07:57.314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#--------------------------------------------------------------------------\n# 7. INTERPRETABILITY (Grad-CAM)\n#--------------------------------------------------------------------------\n# Applying Grad-CAM to YOLO is complex. We need to target:\n# 1. A specific convolutional layer in the backbone.\n# 2. A \"target\" - for object detection, this could be the score of a specific class for a detected box.\n\nif os.path.exists(BEST_MODEL_PATH):\n    print(\"\\nSetting up Grad-CAM for interpretability...\")\n    # Load the trained YOLO model\n    interp_model = YOLO(BEST_MODEL_PATH)\n    \n    # Find a suitable target layer. This requires inspecting the model architecture.\n    # For YOLOv8, common layers are in `model.model.model` (the backbone part)\n    # Let's try to find the last conv layer of the backbone part.\n    # This is heuristic and might need adjustment based on the exact YOLOv8 variant.\n    # The YOLOv8 model object from ultralytics is a bit complex to introspect directly for layer names.\n    # We'll try to access the PyTorch model directly.\n    \n    # The actual PyTorch model is often accessible via `interp_model.model`\n    # You might need to print(interp_model.model) to see its structure.\n    # For YOLOv8, a common target is one of the last C2f or Conv layers in the PPYOLOE backbone.\n    # e.g., model.model[-2] (second to last module) or model.model.model[idx_of_conv_layer]\n\n    # A common choice is often the last layer of the feature extraction backbone.\n    # For YOLOv8, it's often something like `model.model.model[9]` (the last SPPF/C2f before detection heads)\n    # Or `model.model.model[7]` or `model.model.model[5]` (earlier C2f blocks).\n    # This requires careful inspection of `print(interp_model.model)`\n    \n    # For demonstration, let's assume a target layer index. YOU MUST VERIFY THIS.\n    # `print(interp_model.model)` to see the layer structure.\n    # For `yolov8s.pt`, `model.model.model[9]` (a C2f module) or `model.model.model[8]` (a Conv layer) might be suitable.\n    # Let's try the last Conv layer in the last C2f module of the backbone.\n    # For yolov8s.pt, this might be interp_model.model.model[9].cv2.conv\n    \n    # Example: if interp_model.model.model[9] is a C2f module:\n    # target_layers = [interp_model.model.model[9].cv2.conv]\n    # Or, more generally, if model.model[X] is the backbone:\n    # target_layers = [interp_model.model.model[SOME_INDEX_OF_LAST_CONV_IN_BACKBONE]]\n    \n    # Let's try a robust way to get a late conv layer from the backbone.\n    # The backbone layers are typically before the detection head, often up to index 9 or 10 for yolov8s\n    # The detection head starts around model.model.model[15] onwards with Detect module at model.model.model[22]\n    \n    # Heuristic: find last Conv layer before index ~10-12\n    # This part is tricky and model-dependent.\n    # `print(interp_model.model)` will show you the modules.\n    # For yolov8s, model.model.model[9] is a C2f module. Its last conv is model.model.model[9].cv2.conv\n    # Let's target that.\n\n    try:\n        # This is specific to yolov8s.pt architecture structure. May need adjustment.\n        # print(interp_model.model) # UNCOMMENT TO INSPECT MODEL STRUCTURE\n        target_layers = [interp_model.model.model[9].cv2.conv] # Example target layer from C2f\n        print(f\"Selected Grad-CAM target layer: {target_layers[0]}\")\n    except Exception as e:\n        print(f\"Could not automatically select target layer: {e}. Using a fallback (may not be optimal).\")\n        # Fallback, try an earlier conv layer in the backbone\n        # print(interp_model.model.model[6]) # This is a C2f module for yolov8s\n        target_layers = [interp_model.model.model[6].cv2.conv]\n        print(f\"Selected Fallback Grad-CAM target layer: {target_layers[0]}\")\n\n\n    # We need a custom target for object detection.\n    # The target function should return a scalar score for the class we're interested in for specific boxes.\n    # `FasterRCNNBoxScoreTarget` from pytorch-grad-cam is for FasterRCNN. We adapt its idea.\n    # We want to get the score of the 'tuberculosis' class for the highest scoring bounding box.\n\n    class YOLOBoxScoreTarget:\n        def __init__(self, labels, bounding_boxes):\n            self.labels = labels\n            self.bounding_boxes = bounding_boxes\n\n        def __call__(self, model_output):\n            # model_output for YOLO is a list of Tensors [batch, num_obj, (xyxy, conf, cls)]\n            # We are interested in the confidence of our CLASS_ID for the given boxes.\n            # This is simplified. A real implementation might need to parse model_output more carefully.\n            # The `pytorch-grad-cam` library expects a scalar output from this target.\n            # Let's consider the sum of confidences for the target class in the detected boxes.\n            \n            output = model_output[0] # Assuming first element of list is the detections tensor\n                                     # Format [num_detected_boxes, 4 (xyxy) + 1 (conf) + num_classes (logits/probs)]\n                                     # Or for ultralytics output: a list of Results objects.\n                                     # For YOLO model_output can be complex during CAM tracing.\n                                     # Usually, it's [predictions, features_from_intermediate_layers]\n\n            # This is a placeholder. The actual output format during CAM tracing needs to be handled.\n            # For YOLO, `model_output` in the CAM context is often the direct output of the conv layers\n            # before they are processed by the detection head.\n            # The CAM library itself handles getting class scores. We need to tell it *which* class.\n            # The simplest target is ClassifierOutputTarget for the desired class_id.\n            # This will give a general heatmap for \"where is tuberculosis?\"\n            \n            # Let's use a simpler approach for demonstration: use ClassifierOutputTarget.\n            # This treats the problem as \"where are features related to CLASS_ID most active?\"\n            return ClassifierOutputTarget(CLASS_ID)(model_output)\n\n\n    cam = GradCAM(model=interp_model.model, target_layers=target_layers, use_cuda=torch.cuda.is_available())\n    \n    # Get some validation images to run CAM on\n    val_image_files = glob.glob(os.path.join(PROCESSED_DATA_DIR, 'images', 'val', '*.png'))\n    if val_image_files:\n        sample_cam_images = random.sample(val_image_files, min(3, len(val_image_files)))\n\n        for img_path in sample_cam_images:\n            print(f\"Generating Grad-CAM for: {img_path}\")\n            # Load and preprocess image for CAM\n            rgb_img = cv2.imread(img_path, 1)[:, :, ::-1] # Read BGR, convert to RGB\n            rgb_img = np.float32(rgb_img) / 255\n            \n            # Preprocess for YOLO input:\n            # YOLO's `predict` handles preprocessing. For CAM, we need to match it.\n            # Standard normalization:\n            input_tensor = preprocess_image(rgb_img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n            input_tensor = input_tensor.to(DEVICE)\n\n            # Define targets for CAM. We want CAM for our 'tuberculosis' class.\n            # If detections are made, we can refine targets to specific boxes.\n            # For now, let's use a general class target.\n            # This requires that model_output of CAM call can be handled by ClassifierOutputTarget.\n            # For YOLO, the raw output of conv layers is often passed.\n            \n            # Make a prediction to see if there are any boxes (optional, but good for context)\n            preds = interp_model.predict(img_path, verbose=False)\n            detected_boxes_for_class = []\n            if preds and preds[0].boxes:\n                for box in preds[0].boxes:\n                    if int(box.cls) == CLASS_ID and float(box.conf) > 0.3: # If our class is detected\n                        detected_boxes_for_class.append(box.xyxyn.cpu().numpy()[0]) # Normalized xyxy\n\n            # If we have detected boxes, we could try to make targets specific to them.\n            # For simplicity, we use ClassifierOutputTarget. This asks \"where are features for this class?\"\n            # The cam `__call__` method for YOLO might need specific handling of `model_output` if `ClassifierOutputTarget` doesn't work directly.\n            # The `pytorch-grad-cam` library has examples for YOLO. Often it involves wrapping the model\n            # or providing a custom `reshape_transform`.\n\n            # For YOLO models in `pytorch-grad-cam`, often a `reshape_transform` is needed\n            # if the output of `target_layers` is not [batch, channels, height, width].\n            # Example transform (may need adjustment):\n            def yolo_reshape_transform(x):\n                # x is a list of feature maps from different FPN levels if target_layers is a list\n                # If target_layers is a single layer, x is its output\n                # Assuming x is [batch, channels, height, width] from the target_layer\n                # If it's more complex (e.g. list of tensors for FPN), this needs to be handled.\n                target_size = x[-1].size() # if x is a list, take last one (highest res usually)\n                # This is a guess, you need to check the actual output shape of your target_layer\n                # activation = x[-1].view(target_size[0], target_size[1], target_size[2]*target_size[3]) # Example\n                return x # If already in correct shape or CAM handles it.\n\n            # cam.reshape_transform = yolo_reshape_transform # Uncomment and adapt if needed\n\n            # Generate CAM:\n            # The targets should be a list of target_type objects.\n            # Example: targets = [ClassifierOutputTarget(CLASS_ID)]\n            # If the model output from the target layer is multi-scale (FPN), this can be complex.\n            try:\n                grayscale_cam = cam(input_tensor=input_tensor,\n                                    targets=[ClassifierOutputTarget(CLASS_ID)]) \n                                    # aug_smooth=True, eigen_smooth=True might help\n                \n                # Take the CAM for the first image in the batch\n                grayscale_cam = grayscale_cam[0, :] \n                \n                # Overlay CAM on original image\n                visualization = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)\n                \n                # Plot original, CAM, and overlay\n                fig, axs = plt.subplots(1, 3, figsize=(18, 6))\n                axs[0].imshow(rgb_img)\n                axs[0].set_title(f\"Original: {os.path.basename(img_path)}\")\n                axs[0].axis('off')\n\n                axs[1].imshow(grayscale_cam, cmap='jet')\n                axs[1].set_title(\"Grad-CAM\")\n                axs[1].axis('off')\n\n                axs[2].imshow(visualization)\n                axs[2].set_title(\"Grad-CAM Overlay\")\n                axs[2].axis('off')\n                \n                # Draw ground truth and predicted boxes for context\n                # Ground truth:\n                label_path_gt = img_path.replace('/images/', '/labels/').replace('.png', '.txt')\n                if os.path.exists(label_path_gt):\n                    with open(label_path_gt, 'r') as f_gt:\n                        for line in f_gt:\n                            _, x_c, y_c, w, h = map(float, line.split())\n                            h_img, w_img = rgb_img.shape[:2]\n                            x1 = int((x_c - w/2) * w_img)\n                            y1 = int((y_c - h/2) * h_img)\n                            x2 = int((x_c + w/2) * w_img)\n                            y2 = int((y_c + h/2) * h_img)\n                            cv2.rectangle(visualization, (x1,y1), (x2,y2), (0,255,0), 2) # Green for GT\n                # Predicted boxes:\n                if detected_boxes_for_class:\n                     for box_norm in detected_boxes_for_class:\n                        h_img, w_img = rgb_img.shape[:2]\n                        x1, y1, x2, y2 = box_norm\n                        x1, y1, x2, y2 = int(x1*w_img), int(y1*h_img), int(x2*w_img), int(y2*h_img)\n                        cv2.rectangle(visualization, (x1,y1), (x2,y2), (255,0,0), 2) # Red for Pred\n\n                axs[2].imshow(visualization) # Show again with boxes\n                plt.tight_layout()\n                plt.show()\n\n                # Save the Grad-CAM overlay\n                cam_save_dir = os.path.join(OUTPUT_DIR, 'grad_cam_outputs')\n                os.makedirs(cam_save_dir, exist_ok=True)\n                cam_save_path = os.path.join(cam_save_dir, f\"grad_cam_{os.path.basename(img_path)}\")\n                cv2.imwrite(cam_save_path, cv2.cvtColor(visualization, cv2.COLOR_RGB2BGR))\n                print(f\"Grad-CAM saved to {cam_save_path}\")\n\n            except Exception as e_cam:\n                print(f\"Error generating Grad-CAM for {img_path}: {e_cam}\")\n                import traceback\n                traceback.print_exc()\n                print(\"Grad-CAM for YOLO can be tricky. Ensure target_layers and targets are correctly set up.\")\n                print(\"You might need a reshape_transform or a more specific YOLO target for `pytorch-grad-cam`.\")\n\n    else:\n        print(\"No validation images found to generate Grad-CAM for.\")\nelse:\n    print(\"Skipping Grad-CAM as best model path not found.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-27T09:50:32.287795Z","iopub.execute_input":"2025-05-27T09:50:32.288541Z","iopub.status.idle":"2025-05-27T09:50:32.380564Z","shell.execute_reply.started":"2025-05-27T09:50:32.288508Z","shell.execute_reply":"2025-05-27T09:50:32.379668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#--------------------------------------------------------------------------\n# 8. CONCLUSION AND NEXT STEPS\n#--------------------------------------------------------------------------\nprint(\"\\nNotebook execution finished.\")\nprint(\"Summary of outputs:\")\nprint(f\"  - Processed dataset: {PROCESSED_DATA_DIR}\")\nprint(f\"  - Training runs: {os.path.join(OUTPUT_DIR, 'yolo_training_runs')}\")\nprint(f\"  - Validation runs: {os.path.join(OUTPUT_DIR, 'yolo_validation_runs')}\")\nprint(f\"  - Prediction samples: {os.path.join(OUTPUT_DIR, 'yolo_prediction_outputs')}\")\nif os.path.exists(BEST_MODEL_PATH):\n    print(f\"  - Best trained model: {BEST_MODEL_PATH}\")\nif os.path.exists(os.path.join(OUTPUT_DIR, 'grad_cam_outputs')):\n    print(f\"  - Grad-CAM outputs: {os.path.join(OUTPUT_DIR, 'grad_cam_outputs')}\")\n\nprint(\"\\nNext Steps for your Research (SSEBABI SAM):\")\nprint(\"1. Dataset Acquisition: Find or meticulously annotate a high-quality Chest X-Ray dataset with BOUNDING BOXES for Tuberculosis.\")\nprint(\"   - This is the MOST CRITICAL step. The RSNA Pneumonia dataset was a placeholder.\")\nprint(\"   - Ensure diverse cases: different stages of TB, co-morbidities, varied image quality if possible.\")\nprint(\"2. Data Augmentation: Experiment with more advanced augmentations suitable for medical images (e.g., brightness, contrast, elastic deformations if appropriate). YOLOv8 has built-in augmentations, but you can customize.\")\nprint(\"3. Hyperparameter Tuning: Systematically tune epochs, batch size, learning rate, optimizer, augmentation settings, and YOLOv8 model size (n, s, m, l, x).\")\nprint(\"4. Cross-Validation: Implement k-fold cross-validation for more robust performance estimation, especially if your dataset is not extremely large.\")\nprint(\"5. Interpretability Refinement: Deepen the Grad-CAM analysis for YOLOv8. This might involve:\")\nprint(\"   - Carefully selecting target layers (inspect model architecture thoroughly).\")\nprint(\"   - Implementing custom `reshape_transform` or `FasterRCNNBoxScoreTarget`-like targets specifically for YOLO's output structure if `ClassifierOutputTarget` is too general.\")\nprint(\"   - Exploring other XAI methods like LIME or SHAP if applicable, though Grad-CAM is good for visual tasks.\")\nprint(\"6. Comparison: Compare your YOLOv8 model's performance (accuracy, mAP, inference speed) against other state-of-the-art TB detection models or established radiological practices (if possible).\")\nprint(\"7. Clinical Relevance: Discuss the potential clinical impact, limitations, and ethical considerations of your model with medical professionals.\")\nprint(\"8. Paper Writing: Document your methodology, dataset, experiments, results, and discussions thoroughly as per your research proposal.\")\n\nprint(\"\\nGood luck with your Master's research, Sam!\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null}]}