{"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":29653,"databundleVersionId":2420395,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# BISMILLAH","metadata":{}},{"cell_type":"code","source":"# Step 1: Import Libraries dan Setup Environment\nimport os\nimport pandas as pd\nimport numpy as np\nimport pydicom\nimport cv2\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.svm import SVC\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.decomposition import PCA\nimport joblib\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Set random seed untuk reproducibility\nnp.random.seed(42)\n\nprint(\"Libraries imported successfully!\")\nprint(\"OpenCV version:\", cv2.__version__)\nprint(\"NumPy version:\", np.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T13:59:42.968174Z","iopub.execute_input":"2025-06-02T13:59:42.968599Z","iopub.status.idle":"2025-06-02T13:59:42.977349Z","shell.execute_reply.started":"2025-06-02T13:59:42.96857Z","shell.execute_reply":"2025-06-02T13:59:42.97633Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 2: Load Data dan Sampling 30%\n\n# Path ke dataset (sesuaikan dengan lokasi dataset Anda)\nBASE_PATH = \"/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification\"\nTRAIN_PATH = os.path.join(BASE_PATH, \"train\")\n\n# Load CSV file dengan label\ntrain_labels = pd.read_csv(os.path.join(BASE_PATH, \"train_labels.csv\"))\nprint(f\"Original dataset size: {len(train_labels)} patients\")\nprint(\"Label distribution:\")\nprint(train_labels['MGMT_value'].value_counts())\n\n# Sampling 30% data secara stratified untuk menjaga proporsi label\ntrain_sample, _ = train_test_split(\n    train_labels, \n    test_size=0.7, \n    stratify=train_labels['MGMT_value'], \n    random_state=42\n)\n\nprint(f\"\\nSampled dataset size: {len(train_sample)} patients (30%)\")\nprint(\"Sampled label distribution:\")\nprint(train_sample['MGMT_value'].value_counts())\n\n# Reset index\ntrain_sample = train_sample.reset_index(drop=True)\n\n# List semua modalitas MRI yang akan digunakan\nMODALITIES = ['FLAIR', 'T1w', 'T1wCE', 'T2w']\n\nprint(f\"\\nModalities to process: {MODALITIES}\")\nprint(f\"Total combinations: {len(train_sample)} patients × {len(MODALITIES)} modalities\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:00:06.434175Z","iopub.execute_input":"2025-06-02T14:00:06.434559Z","iopub.status.idle":"2025-06-02T14:00:06.48415Z","shell.execute_reply.started":"2025-06-02T14:00:06.434532Z","shell.execute_reply":"2025-06-02T14:00:06.482942Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 3: Functions untuk Load dan Process DICOM\n\ndef load_dicom_series(patient_id, modality, base_path):\n    \"\"\"\n    Load semua DICOM files untuk satu patient dan satu modality\n    \"\"\"\n    modality_path = os.path.join(base_path, str(patient_id).zfill(5), modality)\n    \n    if not os.path.exists(modality_path):\n        print(f\"Warning: Path tidak ditemukan {modality_path}\")\n        return None\n    \n    dicom_files = []\n    filenames = os.listdir(modality_path)\n    \n    # Sort files berdasarkan nama untuk konsistensi\n    filenames.sort()\n    \n    for filename in filenames:\n        if filename.endswith('.dcm'):\n            try:\n                file_path = os.path.join(modality_path, filename)\n                dcm = pydicom.dcmread(file_path)\n                dicom_files.append(dcm.pixel_array)\n            except Exception as e:\n                print(f\"Error reading {file_path}: {e}\")\n                continue\n    \n    if not dicom_files:\n        return None\n    \n    # Stack semua slices menjadi 3D array\n    volume = np.stack(dicom_files, axis=0)\n    return volume\n\ndef extract_middle_slice(volume):\n    \"\"\"\n    Extract middle slice dari 3D volume untuk representasi 2D\n    \"\"\"\n    if volume is None or len(volume) == 0:\n        return None\n    \n    middle_idx = len(volume) // 2\n    return volume[middle_idx]\n\ndef load_patient_data(patient_id, modalities, base_path):\n    \"\"\"\n    Load data untuk satu patient dari semua modalities\n    \"\"\"\n    patient_data = {}\n    \n    for modality in modalities:\n        volume = load_dicom_series(patient_id, modality, base_path)\n        if volume is not None:\n            # Ambil middle slice sebagai representasi\n            middle_slice = extract_middle_slice(volume)\n            patient_data[modality] = middle_slice\n        else:\n            patient_data[modality] = None\n            \n    return patient_data\n\n# Test function dengan satu patient\nprint(\"Testing DICOM loading function...\")\ntest_patient_id = train_sample.iloc[0]['BraTS21ID']\ntest_data = load_patient_data(test_patient_id, MODALITIES, TRAIN_PATH)\n\nfor modality in MODALITIES:\n    if test_data[modality] is not None:\n        print(f\"{modality}: Shape {test_data[modality].shape}\")\n    else:\n        print(f\"{modality}: Not found or error\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:00:28.919122Z","iopub.execute_input":"2025-06-02T14:00:28.919734Z","iopub.status.idle":"2025-06-02T14:00:30.357129Z","shell.execute_reply.started":"2025-06-02T14:00:28.919703Z","shell.execute_reply":"2025-06-02T14:00:30.356057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 4: Image Preprocessing Functions (berdasarkan kode Anda)\n\ndef resize_image(image, size=(256, 256)):\n    \"\"\"Resize image dengan interpolasi yang sesuai\"\"\"\n    if image is None:\n        return None\n    resized = cv2.resize(image, size, interpolation=cv2.INTER_AREA)\n    return resized\n\ndef apply_clahe(image, clip_limit=2.0, tile_grid_size=(8, 8)):\n    \"\"\"\n    CLAHE (Contrast Limited Adaptive Histogram Equalization)\n    untuk memperjelas citra medis grayscale.\n    \"\"\"\n    if image is None:\n        return None\n        \n    if image.dtype != np.uint8:\n        # Normalize ke rentang 0-255 dan konversi ke uint8\n        image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n    \n    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)\n    enhanced = clahe.apply(image)\n    return enhanced\n\ndef apply_unsharp_mask(image, kernel_size=(5, 5), sigma=1.0, amount=1.5, threshold=0):\n    \"\"\"\n    Terapkan unsharp masking ke citra grayscale.\n    \"\"\"\n    if image is None:\n        return None\n        \n    # Pastikan gambar dalam format uint8\n    if image.dtype != np.uint8:\n        image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n\n    # Blur citra\n    blurred = cv2.GaussianBlur(image, kernel_size, sigma)\n\n    # Hitung mask (detail edges)\n    sharpened = float(amount + 1) * image - float(amount) * blurred\n    sharpened = np.maximum(sharpened, 0)\n    sharpened = np.minimum(sharpened, 255)\n    sharpened = sharpened.round().astype(np.uint8)\n\n    if threshold > 0:\n        # Terapkan thresholding agar hanya bagian signifikan yang diasah\n        low_contrast_mask = np.abs(image - blurred) < threshold\n        np.copyto(sharpened, image, where=low_contrast_mask)\n\n    return sharpened\n\ndef apply_interpolation(image, target_size=(256, 256), method=cv2.INTER_LANCZOS4):\n    \"\"\"\n    Interpolasi gambar ke ukuran target menggunakan metode tertentu.\n    \"\"\"\n    if image is None:\n        return None\n        \n    if image.dtype != np.uint8:\n        image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n    \n    resized = cv2.resize(image, target_size, interpolation=method)\n    return resized\n\ndef normalize_image(image):\n    \"\"\"Normalize image ke range [0, 1]\"\"\"\n    if image is None:\n        return None\n        \n    image = image.astype(np.float32)\n    min_val = np.min(image)\n    max_val = np.max(image)\n    if max_val - min_val == 0:\n        return np.zeros_like(image)  # menghindari pembagian nol\n    return (image - min_val) / (max_val - min_val)\n\ndef preprocess_image(image, target_size=(256, 256)):\n    \"\"\"\n    Pipeline preprocessing lengkap untuk satu image\n    \"\"\"\n    if image is None:\n        return None\n    \n    # Step 1: Resize\n    processed = resize_image(image, target_size)\n    \n    # Step 2: CLAHE Enhancement\n    processed = apply_clahe(processed)\n    \n    # Step 3: Unsharp Masking\n    processed = apply_unsharp_mask(processed)\n    \n    # Step 4: Final Interpolation\n    processed = apply_interpolation(processed, target_size)\n    \n    # Step 5: Normalization\n    processed = normalize_image(processed)\n    \n    return processed\n\nprint(\"Preprocessing functions defined successfully!\")\n\n# Test preprocessing dengan sample image\nif test_data[MODALITIES[0]] is not None:\n    original = test_data[MODALITIES[0]]\n    processed = preprocess_image(original)\n    print(f\"Original shape: {original.shape}, Processed shape: {processed.shape}\")\n    print(f\"Original range: [{np.min(original):.2f}, {np.max(original):.2f}]\")\n    print(f\"Processed range: [{np.min(processed):.2f}, {np.max(processed):.2f}]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:00:50.905434Z","iopub.execute_input":"2025-06-02T14:00:50.905892Z","iopub.status.idle":"2025-06-02T14:00:51.00378Z","shell.execute_reply.started":"2025-06-02T14:00:50.905855Z","shell.execute_reply":"2025-06-02T14:00:51.002123Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 5: Feature Extraction untuk Traditional Machine Learning\n\ndef extract_statistical_features(image):\n    \"\"\"\n    Extract statistical features dari image\n    \"\"\"\n    if image is None:\n        return np.zeros(8)  # Return zero features if image is None\n    \n    features = []\n    \n    # Basic statistical features\n    features.append(np.mean(image))      # Mean intensity\n    features.append(np.std(image))       # Standard deviation\n    features.append(np.min(image))       # Minimum intensity\n    features.append(np.max(image))       # Maximum intensity\n    features.append(np.median(image))    # Median intensity\n    features.append(np.percentile(image, 25))  # 25th percentile\n    features.append(np.percentile(image, 75))  # 75th percentile\n    features.append(np.var(image))       # Variance\n    \n    return np.array(features)\n\ndef extract_texture_features(image):\n    \"\"\"\n    Extract texture features menggunakan Gray Level Co-occurrence Matrix (GLCM) approximation\n    \"\"\"\n    if image is None:\n        return np.zeros(4)\n    \n    # Convert to uint8 for texture analysis\n    if image.dtype != np.uint8:\n        image_uint8 = (image * 255).astype(np.uint8)\n    else:\n        image_uint8 = image\n    \n    # Simple texture measures\n    features = []\n    \n    # Gradient-based features\n    grad_x = cv2.Sobel(image_uint8, cv2.CV_64F, 1, 0, ksize=3)\n    grad_y = cv2.Sobel(image_uint8, cv2.CV_64F, 0, 1, ksize=3)\n    gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2)\n    \n    features.append(np.mean(gradient_magnitude))  # Average gradient magnitude\n    features.append(np.std(gradient_magnitude))   # Gradient variation\n    \n    # Laplacian (measure of edges/details)\n    laplacian = cv2.Laplacian(image_uint8, cv2.CV_64F)\n    features.append(np.mean(np.abs(laplacian)))   # Average edge response\n    features.append(np.std(laplacian))            # Edge variation\n    \n    return np.array(features)\n\ndef extract_shape_features(image):\n    \"\"\"\n    Extract shape-related features dari image\n    \"\"\"\n    if image is None:\n        return np.zeros(3)\n    \n    # Binary thresholding untuk shape analysis\n    if image.dtype != np.uint8:\n        image_uint8 = (image * 255).astype(np.uint8)\n    else:\n        image_uint8 = image\n        \n    # Otsu thresholding\n    _, binary = cv2.threshold(image_uint8, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n    \n    features = []\n    \n    # Area of non-zero regions\n    non_zero_pixels = np.count_nonzero(binary)\n    total_pixels = binary.shape[0] * binary.shape[1]\n    features.append(non_zero_pixels / total_pixels)  # Relative area\n    \n    # Perimeter approximation using edge detection\n    edges = cv2.Canny(image_uint8, 50, 150)\n    perimeter_pixels = np.count_nonzero(edges)\n    features.append(perimeter_pixels / total_pixels)  # Relative perimeter\n    \n    # Compactness approximation\n    if perimeter_pixels > 0:\n        compactness = (4 * np.pi * non_zero_pixels) / (perimeter_pixels ** 2)\n        features.append(compactness)\n    else:\n        features.append(0)\n    \n    return np.array(features)\n\ndef extract_all_features(image):\n    \"\"\"\n    Combine semua feature extraction methods\n    \"\"\"\n    if image is None:\n        return np.zeros(15)  # 8 + 4 + 3 = 15 features total\n    \n    stat_features = extract_statistical_features(image)\n    texture_features = extract_texture_features(image)\n    shape_features = extract_shape_features(image)\n    \n    # Combine all features\n    all_features = np.concatenate([stat_features, texture_features, shape_features])\n    \n    return all_features\n\nprint(\"Feature extraction functions defined successfully!\")\n\n# Test feature extraction\nif test_data[MODALITIES[0]] is not None:\n    processed_img = preprocess_image(test_data[MODALITIES[0]])\n    features = extract_all_features(processed_img)\n    print(f\"Extracted {len(features)} features\")\n    print(f\"Feature sample: {features[:5]}\")  # Show first 5 features","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:01:04.950172Z","iopub.execute_input":"2025-06-02T14:01:04.950626Z","iopub.status.idle":"2025-06-02T14:01:05.008555Z","shell.execute_reply.started":"2025-06-02T14:01:04.950599Z","shell.execute_reply":"2025-06-02T14:01:05.007185Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 6: Process semua data dan create dataset\n\ndef process_all_patients(sample_df, modalities, base_path):\n    \"\"\"\n    Process semua patients dan extract features untuk setiap modality\n    \"\"\"\n    # Initialize lists untuk menyimpan data\n    all_features = []\n    all_labels = []\n    all_patient_ids = []\n    all_modalities = []\n    \n    print(f\"Processing {len(sample_df)} patients...\")\n    \n    for idx, row in sample_df.iterrows():\n        patient_id = row['BraTS21ID']\n        label = row['MGMT_value']\n        \n        if idx % 10 == 0:  # Progress indicator\n            print(f\"Processing patient {idx+1}/{len(sample_df)}: {patient_id}\")\n        \n        # Load patient data\n        patient_data = load_patient_data(patient_id, modalities, base_path)\n        \n        # Process setiap modality untuk patient ini\n        for modality in modalities:\n            image = patient_data[modality]\n            \n            if image is not None:\n                # Preprocess image\n                processed_image = preprocess_image(image)\n                \n                # Extract features\n                features = extract_all_features(processed_image)\n                \n                # Store data\n                all_features.append(features)\n                all_labels.append(label)\n                all_patient_ids.append(patient_id)\n                all_modalities.append(modality)\n            else:\n                print(f\"Warning: No data for {patient_id} - {modality}\")\n    \n    # Convert ke numpy arrays\n    X = np.array(all_features)\n    y = np.array(all_labels)\n    \n    # Create metadata DataFrame\n    metadata = pd.DataFrame({\n        'patient_id': all_patient_ids,\n        'modality': all_modalities,\n        'label': all_labels\n    })\n    \n    return X, y, metadata\n\n# Process semua data\nprint(\"Starting data processing...\")\nX_all, y_all, metadata_all = process_all_patients(train_sample, MODALITIES, TRAIN_PATH)\n\nprint(f\"\\nData processing completed!\")\nprint(f\"Total samples: {len(X_all)}\")\nprint(f\"Feature dimension: {X_all.shape[1]}\")\nprint(f\"Label distribution: {np.bincount(y_all)}\")\nprint(f\"Modalities distribution:\")\nprint(metadata_all['modality'].value_counts())\n\n# Check for any NaN or infinite values\nprint(f\"\\nData quality check:\")\nprint(f\"NaN values in features: {np.isnan(X_all).sum()}\")\nprint(f\"Infinite values in features: {np.isinf(X_all).sum()}\")\n\n# Handle NaN/Inf values if any\nif np.isnan(X_all).sum() > 0 or np.isinf(X_all).sum() > 0:\n    print(\"Cleaning data...\")\n    # Replace NaN with 0 and Inf with large values\n    X_all = np.nan_to_num(X_all, nan=0.0, posinf=1e10, neginf=-1e10)\n    print(\"Data cleaned!\")\n\n# Save processed data untuk backup\nprint(\"Saving processed data...\")\nnp.save('X_features.npy', X_all)\nnp.save('y_labels.npy', y_all)\nmetadata_all.to_csv('metadata.csv', index=False)\nprint(\"Data saved successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:01:16.19455Z","iopub.execute_input":"2025-06-02T14:01:16.19492Z","iopub.status.idle":"2025-06-02T14:21:08.319551Z","shell.execute_reply.started":"2025-06-02T14:01:16.194894Z","shell.execute_reply":"2025-06-02T14:21:08.318181Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 7: Train models untuk setiap modality secara terpisah\n\ndef train_modality_model(X_features, y_labels, modality_mask, modality_name, test_size=0.2):\n    \"\"\"\n    Train model untuk satu modality specific\n    \"\"\"\n    print(f\"\\n=== Training model untuk {modality_name} ===\")\n    \n    # Filter data untuk modality ini\n    X_mod = X_features[modality_mask]\n    y_mod = y_labels[modality_mask]\n    \n    print(f\"Samples untuk {modality_name}: {len(X_mod)}\")\n    print(f\"Label distribution: {np.bincount(y_mod)}\")\n    \n    # Split train-test\n    X_train, X_test, y_train, y_test = train_test_split(\n        X_mod, y_mod, test_size=test_size, random_state=42, stratify=y_mod\n    )\n    \n    # Feature scaling\n    scaler = StandardScaler()\n    X_train_scaled = scaler.fit_transform(X_train)\n    X_test_scaled = scaler.transform(X_test)\n    \n    # Train multiple models\n    models = {\n        'RandomForest': RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced'),\n        'LogisticRegression': LogisticRegression(random_state=42, class_weight='balanced', max_iter=1000),\n        'SVM': SVC(kernel='rbf', random_state=42, class_weight='balanced', probability=True)\n    }\n    \n    results = {}\n    \n    for model_name, model in models.items():\n        print(f\"\\nTraining {model_name}...\")\n        \n        # Train model\n        model.fit(X_train_scaled, y_train)\n        \n        # Predictions\n        y_pred = model.predict(X_test_scaled)\n        y_pred_proba = model.predict_proba(X_test_scaled)[:, 1]  # Probability untuk class 1\n        \n        # Metrics\n        accuracy = accuracy_score(y_test, y_pred)\n        \n        # Store results\n        results[model_name] = {\n            'model': model,\n            'scaler': scaler,\n            'accuracy': accuracy,\n            'y_test': y_test,\n            'y_pred': y_pred,\n            'y_pred_proba': y_pred_proba\n        }\n        \n        print(f\"{model_name} Accuracy: {accuracy:.4f}\")\n        print(f\"Classification Report:\")\n        print(classification_report(y_test, y_pred))\n    \n    # Find best model\n    best_model_name = max(results, key=lambda x: results[x]['accuracy'])\n    best_result = results[best_model_name]\n    \n    print(f\"\\nBest model untuk {modality_name}: {best_model_name} (Accuracy: {best_result['accuracy']:.4f})\")\n    \n    return results, best_model_name, best_result\n\n# Train models untuk setiap modality\nmodality_models = {}\n\nfor modality in MODALITIES:\n    # Create mask untuk modality ini\n    modality_mask = metadata_all['modality'] == modality\n    \n    if modality_mask.sum() > 0:  # Pastikan ada data untuk modality ini\n        # Train models\n        results, best_name, best_result = train_modality_model(\n            X_all, y_all, modality_mask, modality\n        )\n        \n        # Store best model untuk modality ini\n        modality_models[modality] = {\n            'best_model_name': best_name,\n            'model': best_result['model'],\n            'scaler': best_result['scaler'],\n            'accuracy': best_result['accuracy'],\n            'all_results': results\n        }\n    else:\n        print(f\"Warning: No data found for modality {modality}\")\n\nprint(f\"\\n=== Summary of Best Models ===\")\nfor modality, info in modality_models.items():\n    print(f\"{modality}: {info['best_model_name']} (Accuracy: {info['accuracy']:.4f})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:23:47.272348Z","iopub.execute_input":"2025-06-02T14:23:47.272779Z","iopub.status.idle":"2025-06-02T14:23:48.322071Z","shell.execute_reply.started":"2025-06-02T14:23:47.272745Z","shell.execute_reply":"2025-06-02T14:23:48.320374Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 8: Ensemble Prediction System - Voting dari 4 Modalities\n\ndef predict_patient_ensemble(patient_id, modality_models, modalities, base_path):\n    \"\"\"\n    Predict untuk satu patient menggunakan ensemble dari semua modalities\n    \"\"\"\n    # Load patient data\n    patient_data = load_patient_data(patient_id, modalities, base_path)\n    \n    predictions = []\n    probabilities = []\n    modality_results = {}\n    \n    for modality in modalities:\n        if modality in modality_models:\n            image = patient_data[modality]\n            \n            if image is not None:\n                # Preprocess image\n                processed_image = preprocess_image(image)\n                \n                # Extract features\n                features = extract_all_features(processed_image)\n                features = features.reshape(1, -1)  # Reshape untuk single prediction\n                \n                # Get model dan scaler untuk modality ini\n                model = modality_models[modality]['model']\n                scaler = modality_models[modality]['scaler']\n                \n                # Scale features\n                features_scaled = scaler.transform(features)\n                \n                # Predict\n                pred = model.predict(features_scaled)[0]\n                prob = model.predict_proba(features_scaled)[0, 1]  # Probability class 1\n                \n                predictions.append(pred)\n                probabilities.append(prob)\n                \n                modality_results[modality] = {\n                    'prediction': pred,\n                    'probability': prob\n                }\n            else:\n                print(f\"Warning: No data for {patient_id} - {modality}\")\n    \n    if len(predictions) == 0:\n        return None, None, None\n    \n    # Ensemble voting - mayoritas vote\n    final_prediction = 1 if sum(predictions) > len(predictions) / 2 else 0\n    \n    # Average probability\n    avg_probability = np.mean(probabilities)\n    \n    return final_prediction, avg_probability, modality_results\n\ndef evaluate_ensemble(sample_df, modality_models, modalities, base_path):\n    \"\"\"\n    Evaluate ensemble performance pada test set\n    \"\"\"\n    print(\"Evaluating ensemble performance...\")\n    \n    all_true_labels = []\n    all_pred_labels = []\n    all_probabilities = []\n    detailed_results = []\n    \n    for idx, row in sample_df.iterrows():\n        patient_id = row['BraTS21ID']\n        true_label = row['MGMT_value']\n        \n        if idx % 20 == 0:\n            print(f\"Evaluating patient {idx+1}/{len(sample_df)}\")\n        \n        # Get ensemble prediction\n        pred, prob, modality_res = predict_patient_ensemble(\n            patient_id, modality_models, modalities, base_path\n        )\n        \n        if pred is not None:\n            all_true_labels.append(true_label)\n            all_pred_labels.append(pred)\n            all_probabilities.append(prob)\n            \n            detailed_results.append({\n                'patient_id': patient_id,\n                'true_label': true_label,\n                'ensemble_prediction': pred,\n                'ensemble_probability': prob,\n                'modality_results': modality_res\n            })\n    \n    # Calculate metrics\n    ensemble_accuracy = accuracy_score(all_true_labels, all_pred_labels)\n    \n    print(f\"\\n=== Ensemble Performance ===\")\n    print(f\"Ensemble Accuracy: {ensemble_accuracy:.4f}\")\n    print(f\"Total predictions: {len(all_pred_labels)}\")\n    print(f\"\\nConfusion Matrix:\")\n    print(confusion_matrix(all_true_labels, all_pred_labels))\n    print(f\"\\nClassification Report:\")\n    print(classification_report(all_true_labels, all_pred_labels))\n    \n    return ensemble_accuracy, detailed_results\n\n# Evaluate ensemble pada sample data\nprint(\"Starting ensemble evaluation...\")\nensemble_acc, detailed_res = evaluate_ensemble(train_sample, modality_models, MODALITIES, TRAIN_PATH)\n\n# Analyze detailed results\nprint(f\"\\n=== Detailed Analysis ===\")\ncorrect_predictions = sum(1 for res in detailed_res if res['true_label'] == res['ensemble_prediction'])\nprint(f\"Correct predictions: {correct_predictions}/{len(detailed_res)}\")\n\n# Analyze per-modality contribution\nmodality_contributions = {mod: {'correct': 0, 'total': 0} for mod in MODALITIES}\n\nfor result in detailed_res:\n    for modality, mod_result in result['modality_results'].items():\n        modality_contributions[modality]['total'] += 1\n        if mod_result['prediction'] == result['true_label']:\n            modality_contributions[modality]['correct'] += 1\n\nprint(f\"\\nPer-Modality Individual Accuracy:\")\nfor modality, stats in modality_contributions.items():\n    if stats['total'] > 0:\n        acc = stats['correct'] / stats['total']\n        print(f\"{modality}: {acc:.4f} ({stats['correct']}/{stats['total']})\")\n\n# Save detailed results\nresults_df = pd.DataFrame(detailed_res)\nresults_df.to_csv('ensemble_results.csv', index=False)\nprint(\"\\nDetailed results saved to 'ensemble_results.csv'\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:24:13.052943Z","iopub.execute_input":"2025-06-02T14:24:13.053307Z","iopub.status.idle":"2025-06-02T14:37:15.609239Z","shell.execute_reply.started":"2025-06-02T14:24:13.053269Z","shell.execute_reply":"2025-06-02T14:37:15.608301Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 9: Save trained models dan create final prediction function\n\nimport pickle\n\ndef save_models(modality_models, filename_prefix='brain_tumor_model'):\n    \"\"\"\n    Save semua trained models dan scalers\n    \"\"\"\n    print(\"Saving trained models...\")\n    \n    for modality, model_info in modality_models.items():\n        # Save model\n        model_filename = f\"{filename_prefix}_{modality}_model.pkl\"\n        joblib.dump(model_info['model'], model_filename)\n        \n        # Save scaler\n        scaler_filename = f\"{filename_prefix}_{modality}_scaler.pkl\"\n        joblib.dump(model_info['scaler'], scaler_filename)\n        \n        print(f\"Saved {modality}: {model_filename}, {scaler_filename}\")\n    \n    # Save model metadata\n    metadata = {\n        'modalities': list(modality_models.keys()),\n        'model_info': {mod: {'best_model': info['best_model_name'], \n                            'accuracy': info['accuracy']} \n                      for mod, info in modality_models.items()}\n    }\n    \n    with open(f\"{filename_prefix}_metadata.pkl\", 'wb') as f:\n        pickle.dump(metadata, f)\n    \n    print(f\"Model metadata saved to {filename_prefix}_metadata.pkl\")\n\ndef load_models(filename_prefix='brain_tumor_model'):\n    \"\"\"\n    Load saved models untuk prediction\n    \"\"\"\n    print(\"Loading trained models...\")\n    \n    # Load metadata\n    with open(f\"{filename_prefix}_metadata.pkl\", 'rb') as f:\n        metadata = pickle.load(f)\n    \n    loaded_models = {}\n    \n    for modality in metadata['modalities']:\n        # Load model\n        model_filename = f\"{filename_prefix}_{modality}_model.pkl\"\n        model = joblib.load(model_filename)\n        \n        # Load scaler\n        scaler_filename = f\"{filename_prefix}_{modality}_scaler.pkl\"\n        scaler = joblib.load(scaler_filename)\n        \n        loaded_models[modality] = {\n            'model': model,\n            'scaler': scaler,\n            'accuracy': metadata['model_info'][modality]['accuracy']\n        }\n        \n        print(f\"Loaded {modality} model (Accuracy: {loaded_models[modality]['accuracy']:.4f})\")\n    \n    return loaded_models, metadata\n\n# Save current models\nsave_models(modality_models)\n\n# Test loading models\nloaded_models, model_metadata = load_models()\n\nprint(f\"\\n=== Model Summary ===\")\nprint(f\"Total modalities: {len(loaded_models)}\")\nfor modality, info in loaded_models.items():\n    print(f\"{modality}: Accuracy {info['accuracy']:.4f}\")\n\ndef predict_new_patient(patient_id, base_path, trained_models=None, model_prefix='brain_tumor_model'):\n    \"\"\"\n    Function untuk predict patient baru\n    \"\"\"\n    # Load models jika belum di-provide\n    if trained_models is None:\n        trained_models, _ = load_models(model_prefix)\n    \n    modalities = list(trained_models.keys())\n    \n    print(f\"Predicting patient: {patient_id}\")\n    \n    # Load patient data\n    patient_data = load_patient_data(patient_id, modalities, base_path)\n    \n    predictions = []\n    probabilities = []\n    modality_results = {}\n    \n    for modality in modalities:\n        image = patient_data[modality]\n        \n        if image is not None:\n            # Preprocess\n            processed_image = preprocess_image(image)\n            \n            # Extract features\n            features = extract_all_features(processed_image)\n            features = features.reshape(1, -1)\n            \n            # Get model dan scaler\n            model = trained_models[modality]['model']\n            scaler = trained_models[modality]['scaler']\n            \n            # Scale dan predict\n            features_scaled = scaler.transform(features)\n            pred = model.predict(features_scaled)[0]\n            prob = model.predict_proba(features_scaled)[0, 1]\n            \n            predictions.append(pred)\n            probabilities.append(prob)\n            \n            modality_results[modality] = {\n                'prediction': pred,\n                'probability': prob,\n                'confidence': max(model.predict_proba(features_scaled)[0])\n            }\n            \n            print(f\"{modality}: Prediction={pred}, Probability={prob:.4f}\")\n        else:\n            print(f\"Warning: No data available for {modality}\")\n    \n    if len(predictions) == 0:\n        print(\"ERROR: No valid predictions could be made\")\n        return None, None, None\n    \n    # Ensemble decision\n    final_prediction = 1 if sum(predictions) > len(predictions) / 2 else 0\n    avg_probability = np.mean(probabilities)\n    \n    print(f\"\\n=== Final Ensemble Result ===\")\n    print(f\"Individual predictions: {predictions}\")\n    print(f\"Voting result: {sum(predictions)}/{len(predictions)} voted for class 1\")\n    print(f\"Final prediction: {final_prediction}\")\n    print(f\"Average probability: {avg_probability:.4f}\")\n    print(f\"Prediction confidence: {'HIGH' if abs(avg_probability - 0.5) > 0.3 else 'MEDIUM' if abs(avg_probability - 0.5) > 0.1 else 'LOW'}\")\n    \n    return final_prediction, avg_probability, modality_results\n\nprint(\"\\nModel saving and prediction functions ready!\")\nprint(\"You can now use predict_new_patient(patient_id, base_path) to predict new cases\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T14:37:43.211629Z","iopub.execute_input":"2025-06-02T14:37:43.211948Z","iopub.status.idle":"2025-06-02T14:37:43.419317Z","shell.execute_reply.started":"2025-06-02T14:37:43.211924Z","shell.execute_reply":"2025-06-02T14:37:43.418497Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# PROGRESS MINGGU LALU","metadata":{}},{"cell_type":"markdown","source":"# Baca Data & Sampling","metadata":{}},{"cell_type":"markdown","source":"Mengambil data dicom hanya pada folder FLAIR karena ....\n\nLalu melakukan sampling sebanyak 50% dari keseluruhan pasien secara random","metadata":{}},{"cell_type":"code","source":"import os\nimport pydicom\nimport random\nfrom tqdm import tqdm  # pastikan tqdm sudah terinstall\n\n# Path ke folder train\ntrain_path = '/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification/train/'\n\n# Ambil semua folder pasien\nall_patients = [p for p in os.listdir(train_path) if os.path.isdir(os.path.join(train_path, p))]\n\n# Acak dan ambil 10% folder pasien\nsample_size = max(1, int(0.3 * len(all_patients)))\nsampled_patients = random.sample(all_patients, sample_size)\n\n# List untuk menyimpan data DICOM FLAIR\ndicom_data_flair = []\n\n# Iterasi tiap pasien dengan progress bar\nfor patient_id in tqdm(sampled_patients, desc=\"Memuat data DICOM FLAIR\"):\n    flair_path = os.path.join(train_path, patient_id, 'FLAIR')\n    if os.path.isdir(flair_path):\n        for dcm_file in os.listdir(flair_path):\n            if dcm_file.endswith('.dcm'):\n                dcm_path = os.path.join(flair_path, dcm_file)\n                try:\n                    dicom = pydicom.dcmread(dcm_path)\n                    dicom_data_flair.append(dicom)\n                except Exception as e:\n                    print(f\"Gagal membaca {dcm_path}: {e}\")\n\nprint(f\"\\nTotal pasien diambil: {len(sampled_patients)}\")\nprint(f\"Total file DICOM FLAIR dimuat: {len(dicom_data_flair)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:45:28.936641Z","iopub.execute_input":"2025-05-19T12:45:28.937005Z","iopub.status.idle":"2025-05-19T12:48:28.042543Z","shell.execute_reply.started":"2025-05-19T12:45:28.936978Z","shell.execute_reply":"2025-05-19T12:48:28.041512Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dicom_data_flair[0]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:48:28.044703Z","iopub.execute_input":"2025-05-19T12:48:28.045632Z","iopub.status.idle":"2025-05-19T12:48:28.05694Z","shell.execute_reply.started":"2025-05-19T12:48:28.04557Z","shell.execute_reply":"2025-05-19T12:48:28.055651Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Preprocessing","metadata":{}},{"cell_type":"markdown","source":"## Ambil Gambar Pixel","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\n# Simpan hasil ke dalam DataFrame\ndata = []\n\nfor dicom in dicom_data_flair:\n    try:\n        image_array = dicom.pixel_array  # Ambil data citra (2D array)\n        data.append({\n            \"file_path\": dicom.filename,\n            \"image\": image_array\n        })\n    except Exception as e:\n        print(f\"Gagal mengambil pixel_array dari {dicom.filename}: {e}\")\n\ndf_dicom_flair = pd.DataFrame(data)\n\nprint(f\"\\nTotal file disimpan dalam DataFrame: {len(df_dicom_flair)}\")\ndf_dicom_flair.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:48:52.212391Z","iopub.execute_input":"2025-05-19T12:48:52.213729Z","iopub.status.idle":"2025-05-19T12:49:12.010204Z","shell.execute_reply.started":"2025-05-19T12:48:52.213678Z","shell.execute_reply":"2025-05-19T12:49:12.009132Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Menampilkan 1 gambar pertama\nplt.imshow(df_dicom_flair.iloc[1000][\"image\"], cmap='gray')\nplt.title(df_dicom_flair.iloc[1000][\"file_path\"])\nplt.axis('off')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:50:00.69365Z","iopub.execute_input":"2025-05-19T12:50:00.694004Z","iopub.status.idle":"2025-05-19T12:50:00.923701Z","shell.execute_reply.started":"2025-05-19T12:50:00.693978Z","shell.execute_reply":"2025-05-19T12:50:00.921692Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Resize","metadata":{}},{"cell_type":"code","source":"import cv2\nimport numpy as np\n\ndef resize_images(df, image_column='image', size=(256, 256)):\n    resized_images = []\n    for img in df[image_column]:\n        resized = cv2.resize(img, size, interpolation=cv2.INTER_AREA)\n        resized_images.append(resized)\n    df['resized'] = resized_images\n    return df\n\ndf_dicom_flair = resize_images(df_dicom_flair, image_column='image', size=(256, 256))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:50:02.717537Z","iopub.execute_input":"2025-05-19T12:50:02.717888Z","iopub.status.idle":"2025-05-19T12:50:08.26627Z","shell.execute_reply.started":"2025-05-19T12:50:02.717861Z","shell.execute_reply":"2025-05-19T12:50:08.264667Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef show_images_side_by_side(df, col1, col2, index=0):\n    fig, axs = plt.subplots(1, 2, figsize=(10, 5))\n\n    axs[0].imshow(df.iloc[index][col1], cmap='gray')\n    axs[0].set_title(col1)\n    axs[0].axis('off')\n\n    axs[1].imshow(df.iloc[index][col2], cmap='gray')\n    axs[1].set_title(col2)\n    axs[1].axis('off')\n\n    plt.tight_layout()\n    plt.show()\n\nshow_images_side_by_side(df_dicom_flair, 'image', 'resized', index=1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:50:08.268816Z","iopub.execute_input":"2025-05-19T12:50:08.269629Z","iopub.status.idle":"2025-05-19T12:50:08.759549Z","shell.execute_reply.started":"2025-05-19T12:50:08.269593Z","shell.execute_reply":"2025-05-19T12:50:08.75761Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Enhancement Histogram CLAHE","metadata":{},"attachments":{"a82c0f2d-f95b-4cf5-b967-daaf194576d5.png":{"image/png":"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"},"59c72951-c6d6-492d-9479-1e039ce42844.png":{"image/png":"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"},"6bfcb1ed-e2fe-4395-8eac-23119efaccda.png":{"image/png":"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"},"c9e013db-b52a-46c1-9c86-e69b98c9e8a2.png":{"image/png":"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"}}},{"cell_type":"code","source":"import cv2\nimport numpy as np\n\ndef apply_clahe(image, clip_limit=2.0, tile_grid_size=(8, 8)):\n    \"\"\"\n    Melakukan CLAHE (Contrast Limited Adaptive Histogram Equalization)\n    untuk memperjelas citra medis grayscale.\n    \n    Parameter:\n    - clip_limit: batas kontras untuk mencegah noise\n    - tile_grid_size: ukuran patch untuk pengolahan lokal\n    \"\"\"\n    if image.dtype != np.uint8:\n        # Normalize ke rentang 0-255 dan konversi ke uint8\n        image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n    \n    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)\n    enhanced = clahe.apply(image)\n    return enhanced\n\n# Terapkan CLAHE untuk setiap gambar pada kolom 'resized'\ndf_dicom_flair['enhancement'] = df_dicom_flair['resized'].apply(apply_clahe)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:50:08.967882Z","iopub.execute_input":"2025-05-19T12:50:08.968335Z","iopub.status.idle":"2025-05-19T12:50:18.24085Z","shell.execute_reply.started":"2025-05-19T12:50:08.968305Z","shell.execute_reply":"2025-05-19T12:50:18.239786Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_images_side_by_side(df_dicom_flair, 'resized', 'enhancement', index=1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:50:18.242353Z","iopub.execute_input":"2025-05-19T12:50:18.242649Z","iopub.status.idle":"2025-05-19T12:50:18.558725Z","shell.execute_reply.started":"2025-05-19T12:50:18.242627Z","shell.execute_reply":"2025-05-19T12:50:18.557487Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Unsharp Masking","metadata":{},"attachments":{"6c6b0bb4-8a79-44cf-b588-48d2d3a830dc.png":{"image/png":"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"},"ff9a0742-aae8-4a62-86bd-bd46722250bc.png":{"image/png":"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"},"38059a11-3b6e-4b13-89b5-de309f808909.png":{"image/png":"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"},"fee92993-dc61-43ab-b615-db6964f4d757.png":{"image/png":"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"}}},{"cell_type":"code","source":"def apply_unsharp_mask(image, kernel_size=(5, 5), sigma=1.0, amount=1.5, threshold=0):\n    \"\"\"\n    Terapkan unsharp masking ke citra grayscale.\n    \n    Parameters:\n    - kernel_size: ukuran kernel Gaussian blur\n    - sigma: deviasi standar untuk Gaussian blur\n    - amount: seberapa banyak detail ditambahkan kembali (penguatan tepi)\n    - threshold: nilai minimum perubahan untuk diterapkan (hindari noise)\n    \"\"\"\n    # Pastikan gambar dalam format uint8\n    if image.dtype != np.uint8:\n        image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n\n    # Blur citra\n    blurred = cv2.GaussianBlur(image, kernel_size, sigma)\n\n    # Hitung mask (detail edges)\n    sharpened = float(amount + 1) * image - float(amount) * blurred\n    sharpened = np.maximum(sharpened, 0)\n    sharpened = np.minimum(sharpened, 255)\n    sharpened = sharpened.round().astype(np.uint8)\n\n    if threshold > 0:\n        # Terapkan thresholding agar hanya bagian signifikan yang diasah\n        low_contrast_mask = np.abs(image - blurred) < threshold\n        np.copyto(sharpened, image, where=low_contrast_mask)\n\n    return sharpened\n\ndf_dicom_flair['unsharp_masking'] = df_dicom_flair['enhancement'].apply(\n    lambda img: apply_unsharp_mask(img, kernel_size=(5, 5), sigma=1.0, amount=1.5, threshold=0)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:51:16.959748Z","iopub.execute_input":"2025-05-19T12:51:16.960211Z","iopub.status.idle":"2025-05-19T12:51:40.519821Z","shell.execute_reply.started":"2025-05-19T12:51:16.960178Z","shell.execute_reply":"2025-05-19T12:51:40.518692Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_images_side_by_side(df_dicom_flair, 'enhancement', 'unsharp_masking', index=1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:51:40.521458Z","iopub.execute_input":"2025-05-19T12:51:40.52175Z","iopub.status.idle":"2025-05-19T12:51:40.842542Z","shell.execute_reply.started":"2025-05-19T12:51:40.521725Z","shell.execute_reply":"2025-05-19T12:51:40.841363Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Segmentation","metadata":{}},{"cell_type":"code","source":"# def simple_brain_segmentation(image):\n#     \"\"\"\n#     Segmentasi sederhana gambar CT scan otak dengan thresholding dan operasi morfologi.\n    \n#     image: gambar grayscale (uint8)\n#     return: gambar hasil segmentasi (mask biner)\n#     \"\"\"\n#     # Normalisasi dan pastikan uint8\n#     if image.dtype != np.uint8:\n#         image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n\n#     # Blur untuk mengurangi noise\n#     blurred = cv2.GaussianBlur(image, (5, 5), 0)\n\n#     # Otsu Thresholding (otomatis)\n#     _, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)\n\n#     # Invers jika latar belakang lebih terang\n#     if np.mean(image[thresh == 255]) > np.mean(image[thresh == 0]):\n#         thresh = cv2.bitwise_not(thresh)\n\n#     # Operasi morfologi untuk mengisi lubang dan menghilangkan noise kecil\n#     kernel = np.ones((3, 3), np.uint8)\n#     cleaned = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)\n#     cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_OPEN, kernel, iterations=1)\n\n#     return cleaned\n\n# df_dicom_flair['segmentation'] = df_dicom_flair['unsharp_masking'].apply(simple_brain_segmentation)\n\n# def crop_and_resize_brain(image, mask, output_size=(64, 64)):\n#     \"\"\"\n#     Crop bagian otak dari image berdasarkan mask, lalu resize ke ukuran output_size.\n\n#     Parameters:\n#     - image: numpy array gambar grayscale (2D)\n#     - mask: binary mask (2D), hasil segmentasi\n#     - output_size: tuple ukuran (width, height)\n\n#     Returns:\n#     - image hasil crop & resize\n#     \"\"\"\n#     # Cari koordinat non-zero (bagian otak)\n#     coords = cv2.findNonZero(mask)\n#     if coords is None:\n#         # Jika tidak ada area otak terdeteksi, kembalikan hasil resize langsung\n#         resized = cv2.resize(image, output_size, interpolation=cv2.INTER_LINEAR)\n#         return resized\n    \n#     # Hitung bounding rectangle dari area otak\n#     x, y, w, h = cv2.boundingRect(coords)\n    \n#     # Crop area penting\n#     cropped = image[y:y+h, x:x+w]\n\n#     # Resize ke ukuran tetap\n#     resized = cv2.resize(cropped, output_size, interpolation=cv2.INTER_LINEAR)\n\n#     return resized\n\n# df_dicom_flair['cropped'] = df_dicom_flair.apply(\n#     lambda row: crop_and_resize_brain(row['unsharp_masking'], row['segmentation']), axis=1\n# )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:24:39.718923Z","iopub.execute_input":"2025-05-19T12:24:39.719439Z","iopub.status.idle":"2025-05-19T12:25:16.075036Z","shell.execute_reply.started":"2025-05-19T12:24:39.719403Z","shell.execute_reply":"2025-05-19T12:25:16.073507Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# show_images_side_by_side(df_dicom_flair, 'unsharp_masking', 'cropped', index=1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:25:16.629147Z","iopub.execute_input":"2025-05-19T12:25:16.629662Z","iopub.status.idle":"2025-05-19T12:25:16.904811Z","shell.execute_reply.started":"2025-05-19T12:25:16.629625Z","shell.execute_reply":"2025-05-19T12:25:16.903239Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Interpolation","metadata":{}},{"cell_type":"code","source":"import cv2\nimport numpy as np\n\ndef apply_interpolation(image, target_size, method=cv2.INTER_LANCZOS4):\n    \"\"\"\n    Interpolasi gambar ke ukuran target menggunakan metode tertentu.\n    \n    Parameters:\n    - image: array gambar input (grayscale)\n    - target_size: tuple (width, height)\n    - method: metode interpolasi dari OpenCV\n    \"\"\"\n    if image.dtype != np.uint8:\n        image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n    \n    resized = cv2.resize(image, target_size, interpolation=method)\n    return resized\n\ndf_dicom_flair['interpolation'] = df_dicom_flair['unsharp_masking'].apply(\n    lambda img: apply_interpolation(img, target_size=(256, 256), method=cv2.INTER_LANCZOS4)\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:51:52.615897Z","iopub.execute_input":"2025-05-19T12:51:52.616214Z","iopub.status.idle":"2025-05-19T12:51:54.2667Z","shell.execute_reply.started":"2025-05-19T12:51:52.616193Z","shell.execute_reply":"2025-05-19T12:51:54.264979Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_images_side_by_side(df_dicom_flair, 'unsharp_masking', 'interpolation', index=1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:51:59.937607Z","iopub.execute_input":"2025-05-19T12:51:59.937952Z","iopub.status.idle":"2025-05-19T12:52:00.309984Z","shell.execute_reply.started":"2025-05-19T12:51:59.937927Z","shell.execute_reply":"2025-05-19T12:52:00.308365Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Normalization","metadata":{}},{"cell_type":"code","source":"import numpy as np\n\ndef normalize_image(image):\n    image = image.astype(np.float32)\n    min_val = np.min(image)\n    max_val = np.max(image)\n    if max_val - min_val == 0:\n        return np.zeros_like(image)  # menghindari pembagian nol\n    return (image - min_val) / (max_val - min_val)\n\ndf_dicom_flair['normalized'] = df_dicom_flair['interpolation'].apply(normalize_image)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:52:12.41554Z","iopub.execute_input":"2025-05-19T12:52:12.415955Z","iopub.status.idle":"2025-05-19T12:52:23.87961Z","shell.execute_reply.started":"2025-05-19T12:52:12.415927Z","shell.execute_reply":"2025-05-19T12:52:23.878135Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_images_side_by_side(df_dicom_flair, 'interpolation', 'normalized', index=1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:52:23.881087Z","iopub.execute_input":"2025-05-19T12:52:23.88145Z","iopub.status.idle":"2025-05-19T12:52:24.201921Z","shell.execute_reply.started":"2025-05-19T12:52:23.881413Z","shell.execute_reply":"2025-05-19T12:52:24.200188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_dicom_flair","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:52:29.07704Z","iopub.execute_input":"2025-05-19T12:52:29.077515Z","iopub.status.idle":"2025-05-19T12:52:32.745057Z","shell.execute_reply.started":"2025-05-19T12:52:29.077478Z","shell.execute_reply":"2025-05-19T12:52:32.743949Z"}},"outputs":[],"execution_count":null}]}