{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14347407,"sourceType":"datasetVersion","datasetId":9160980},{"sourceId":14347655,"sourceType":"datasetVersion","datasetId":9161151},{"sourceId":4537,"sourceType":"modelInstanceVersion","modelInstanceId":3329,"modelId":986},{"sourceId":366950,"sourceType":"modelInstanceVersion","modelInstanceId":128169,"modelId":2807}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nos.environ['PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION'] = 'python'\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport warnings\nimport logging\nimport pickle\nimport json\nfrom PIL import Image\nfrom transformers import AutoImageProcessor, AutoModel, SegformerImageProcessor, SegformerForSemanticSegmentation\nfrom sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor\nfrom sklearn.linear_model import Ridge\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom sklearn.model_selection import train_test_split, GridSearchCV\nfrom sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\nfrom tqdm.auto import tqdm\nimport cv2\n\nwarnings.filterwarnings(\"ignore\")\nlogging.getLogger(\"transformers\").setLevel(logging.ERROR)\n\n# ==========================================\n# 1. 路徑設定\n# ==========================================\ndef find_path_globally(target_name, is_file=True):\n    for root, dirs, files in os.walk('/kaggle/input'):\n        if is_file:\n            if target_name in files:\n                return os.path.join(root, target_name)\n        else:\n            if 'config.json' in files and 'dino' in root.lower():\n                return root\n    return None\n\ndef find_img_dir(base_path='/kaggle/input'):\n    train_dir = test_dir = None\n    for root, dirs, files in os.walk(base_path):\n        if 'train' in dirs and 'test' in dirs:\n            parent = root\n            train_dir = os.path.join(parent, 'train')\n            test_dir = os.path.join(parent, 'test')\n            if os.path.exists(train_dir) and os.path.exists(test_dir):\n                train_imgs = [f for f in os.listdir(train_dir) if f.endswith('.jpg')]\n                test_imgs = [f for f in os.listdir(test_dir) if f.endswith('.jpg')]\n                if len(train_imgs) > 0 and len(test_imgs) > 0:\n                    return train_dir, test_dir\n    return None, None\n\n# 模型路徑設定\nMODEL_PATH = find_path_globally(\"config.json\", is_file=False)\nSEG_MODEL_PATH = \"/kaggle/input/segformer/keras/segformer_b0_ade20k_512/3\"\nTRAIN_CSV = \"/kaggle/input/trainchange1/train_chang.csv\"\nMLP_PATH = find_path_globally(\"final_biomass_corrector.pth\")\nif MLP_PATH is None:\n    MLP_PATH = find_path_globally(\"corrector.pth\")\nTEST_CSV = find_path_globally(\"test.csv\")\nTRAIN_IMG_DIR, TEST_IMG_DIR = find_img_dir()\n\nprint(\"=\" * 80)\nprint(\">>> [改良版模型 - SegFormer 語義分割 + 多模型融合]\")\nprint(\"=\" * 80)\n\n# ==========================================\n# 2. 載入模型\n# ==========================================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"使用設備: {device}\")\n\n# 載入 SegFormer 進行語義分割\ntry:\n    seg_processor = SegformerImageProcessor.from_pretrained(\n        SEG_MODEL_PATH, \n        local_files_only=True\n    )\n    seg_model = SegformerForSemanticSegmentation.from_pretrained(\n        SEG_MODEL_PATH, \n        local_files_only=True\n    ).to(device)\n    seg_model.eval()\n    print(\"✅ SegFormer 語義分割模型已載入\")\n    USE_SEGMENTATION = True\nexcept Exception as e:\n    print(f\"⚠️  SegFormer 載入失敗: {e}\")\n    print(\"⚠️  將使用 HSV 顏色空間作為備用方案\")\n    USE_SEGMENTATION = False\n\n# 載入 DINOv2\nprocessor = AutoImageProcessor.from_pretrained(MODEL_PATH, local_files_only=True)\ndino_model = AutoModel.from_pretrained(MODEL_PATH, local_files_only=True).to(device)\ndino_model.eval()\nprint(\"✅ DINOv2 模型已載入\")\n\n# ==========================================\n# 3. 植被遮罩生成（支援多種方法）\n# ==========================================\ndef create_vegetation_mask(img_path, use_segmentation=True):\n    \"\"\"\n    創建植被遮罩，過濾掉泥土等背景\n    \n    方法1: 語義分割（更精確）\n    方法2: HSV 顏色空間（涵蓋綠色、黃色、棕色植被）\n    \"\"\"\n    img = Image.open(img_path).convert(\"RGB\")\n    img_array = np.array(img)\n    \n    if use_segmentation and USE_SEGMENTATION:\n        try:\n            # 使用語義分割識別植被區域\n            inputs = seg_processor(images=img, return_tensors=\"pt\").to(device)\n            \n            with torch.no_grad():\n                outputs = seg_model(**inputs)\n            \n            # 取得分割結果\n            logits = outputs.logits\n            predicted_mask = logits.argmax(dim=1).squeeze().cpu().numpy()\n            \n            # ADE20K 類別: 草地(9), 植物(17), 樹木(4) 等\n            # 完整植被類別列表\n            vegetation_classes = [4, 9, 17, 18, 60, 61, 62, 96, 123, 124, 125]\n            mask = np.isin(predicted_mask, vegetation_classes).astype(np.uint8) * 255\n            \n            # 調整回原圖大小\n            if mask.shape != img_array.shape[:2]:\n                mask = cv2.resize(mask, (img_array.shape[1], img_array.shape[0]))\n            \n        except Exception as e:\n            print(f\"⚠️  語義分割失敗，切換到 HSV 方法: {e}\")\n            use_segmentation = False\n    \n    if not use_segmentation or not USE_SEGMENTATION:\n        # 使用 HSV 顏色空間識別植被（包含綠色、黃色、棕色）\n        hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV)\n        \n        # 綠色範圍（健康植被）\n        lower_green = np.array([25, 20, 20])\n        upper_green = np.array([90, 255, 255])\n        mask_green = cv2.inRange(hsv, lower_green, upper_green)\n        \n        # 黃色範圍（部分枯萎植被）\n        lower_yellow = np.array([15, 20, 20])\n        upper_yellow = np.array([35, 255, 255])\n        mask_yellow = cv2.inRange(hsv, lower_yellow, upper_yellow)\n        \n        # 棕色範圍（枯枝、枯葉）\n        lower_brown = np.array([5, 20, 20])\n        upper_brown = np.array([20, 200, 180])\n        mask_brown = cv2.inRange(hsv, lower_brown, upper_brown)\n        \n        # 合併所有植被遮罩\n        mask = cv2.bitwise_or(mask_green, mask_yellow)\n        mask = cv2.bitwise_or(mask, mask_brown)\n        \n        # 形態學處理，去除雜訊\n        kernel = np.ones((5, 5), np.uint8)\n        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)\n        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)\n    \n    return mask, img\n\ndef apply_mask_to_crop(crop_img, mask_crop):\n    \"\"\"\n    將遮罩應用到切圖上，只保留植被區域\n    \"\"\"\n    crop_array = np.array(crop_img)\n    mask_3channel = np.stack([mask_crop]*3, axis=-1)\n    \n    # 只保留遮罩區域的像素，其他設為黑色\n    masked_crop = np.where(mask_3channel > 0, crop_array, 0)\n    \n    return Image.fromarray(masked_crop.astype(np.uint8))\n\n# ==========================================\n# 4. 改良的切圖函數（帶遮罩）\n# ==========================================\ndef crop_image_8_tiles_with_mask(img_path, crop_size=500, use_mask=True):\n    \"\"\"\n    切成8張並應用植被遮罩\n    \"\"\"\n    # 創建植被遮罩\n    if use_mask:\n        mask, img = create_vegetation_mask(img_path, use_segmentation=USE_SEGMENTATION)\n    else:\n        img = Image.open(img_path).convert(\"RGB\")\n        mask = np.ones((img.size[1], img.size[0]), dtype=np.uint8) * 255\n    \n    crops = []\n    mask_crops = []\n    \n    # 切圖\n    for y in [0, 500]:\n        for x in [0, 500, 1000, 1500]:\n            crop = img.crop((x, y, x + crop_size, y + crop_size))\n            mask_crop = mask[y:y+crop_size, x:x+crop_size]\n            \n            # 應用遮罩\n            masked_crop = apply_mask_to_crop(crop, mask_crop)\n            \n            crops.append(masked_crop)\n            mask_crops.append(mask_crop)\n    \n    return crops, mask_crops\n\n# ==========================================\n# 5. 特徵提取（考慮植被覆蓋率）\n# ==========================================\ndef extract_features_from_crop(crop_img, mask_crop=None, target_size=(518, 518)):\n    \"\"\"\n    提取特徵並加入植被覆蓋率資訊\n    \"\"\"\n    try:\n        img_resized = crop_img.resize(target_size, Image.LANCZOS)\n        img_array = np.array(img_resized).astype(np.float32) / 255.0\n        \n        mean = np.array([0.485, 0.456, 0.406])\n        std = np.array([0.229, 0.224, 0.225])\n        img_normalized = (img_array - mean) / std\n        \n        img_tensor = torch.from_numpy(img_normalized).permute(2, 0, 1).unsqueeze(0).float().to(device)\n        \n        with torch.no_grad():\n            outputs = dino_model(pixel_values=img_tensor)\n        \n        features = outputs.last_hidden_state[:, 0, :].cpu().numpy().flatten()\n        \n        # 加入植被覆蓋率特徵\n        if mask_crop is not None:\n            vegetation_ratio = np.sum(mask_crop > 0) / mask_crop.size\n            features = np.append(features, vegetation_ratio)\n        \n        return features\n        \n    except Exception as e:\n        print(f\"❌ 特徵提取失敗: {e}\")\n        return None\n\n# ==========================================\n# 6. 多模型訓練\n# ==========================================\nprint(\"\\n\" + \"=\" * 80)\nprint(\"🚀 開始訓練 - 使用植被遮罩 + 多模型融合\")\nprint(\"=\" * 80)\n\ntrain_df = pd.read_csv(TRAIN_CSV)\ntarget_cols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n\n# 提取訓練特徵\nfeatures, targets = [], []\nprint(\"\\n正在提取訓練集特徵（帶植被遮罩）...\")\n\nfor idx, row in tqdm(train_df.iterrows(), total=len(train_df), desc=\"訓練集\"):\n    if 'sample_id' in row:\n        img_id = str(row['sample_id'])\n    elif 'sample_id_base' in row:\n        img_id = str(row['sample_id_base'])\n    else:\n        img_id = str(row.iloc[0])\n    \n    img_id = img_id.split('__')[0]\n    if not img_id.endswith('.jpg'):\n        img_id = f\"{img_id}.jpg\"\n    \n    img_path = os.path.join(TRAIN_IMG_DIR, img_id)\n    \n    # 切圖並應用遮罩\n    crops, mask_crops = crop_image_8_tiles_with_mask(img_path, use_mask=True)\n    \n    for crop, mask_crop in zip(crops, mask_crops):\n        feat = extract_features_from_crop(crop, mask_crop)\n        if feat is not None:\n            features.append(feat)\n            targets.append(row[target_cols].values)\n\nX_train_full = np.array(features)\ny_train_full = np.nan_to_num(np.array(targets))\n\nprint(f\"\\n✅ 成功提取 {len(features)} 個訓練樣本\")\nprint(f\"   特徵維度: {X_train_full.shape}\")\n\n# 分割訓練和驗證集\nX_train, X_val, y_train, y_val = train_test_split(\n    X_train_full, y_train_full, test_size=0.15, random_state=42\n)\n\n# ==========================================\n# 7. 訓練多個模型\n# ==========================================\nprint(\"\\n\" + \"=\" * 80)\nprint(\"🔧 訓練多個模型並進行融合\")\nprint(\"=\" * 80)\n\nmodels = {\n    'Ridge': MultiOutputRegressor(Ridge(alpha=1.0)),\n    'RandomForest': MultiOutputRegressor(RandomForestRegressor(n_estimators=100, max_depth=20, random_state=42, n_jobs=-1)),\n    'GradientBoosting': MultiOutputRegressor(GradientBoostingRegressor(n_estimators=100, max_depth=5, random_state=42))\n}\n\ntrained_models = {}\nval_predictions = {}\n\nfor name, model in models.items():\n    print(f\"\\n訓練 {name}...\")\n    model.fit(X_train, y_train)\n    trained_models[name] = model\n    \n    # 驗證集預測\n    y_pred = model.predict(X_val)\n    y_pred = np.clip(y_pred, 0, None)\n    val_predictions[name] = y_pred\n    \n    # 評估\n    rmse = np.sqrt(mean_squared_error(y_val, y_pred))\n    mae = mean_absolute_error(y_val, y_pred)\n    r2 = r2_score(y_val, y_pred)\n    \n    print(f\"  RMSE: {rmse:.4f}\")\n    print(f\"  MAE:  {mae:.4f}\")\n    print(f\"  R²:   {r2:.4f}\")\n\n# 模型融合（加權平均）\nprint(\"\\n\" + \"=\" * 80)\nprint(\"🎯 計算最佳融合權重\")\nprint(\"=\" * 80)\n\n# 簡單平均融合\nensemble_pred = np.mean([val_predictions[name] for name in models.keys()], axis=0)\nensemble_pred = np.clip(ensemble_pred, 0, None)\n\nensemble_rmse = np.sqrt(mean_squared_error(y_val, ensemble_pred))\nensemble_mae = mean_absolute_error(y_val, ensemble_pred)\nensemble_r2 = r2_score(y_val, ensemble_pred)\n\nprint(f\"\\n融合模型表現:\")\nprint(f\"  RMSE: {ensemble_rmse:.4f}\")\nprint(f\"  MAE:  {ensemble_mae:.4f}\")\nprint(f\"  R²:   {ensemble_r2:.4f}\")\n\n# ==========================================\n# 8. 預測測試集\n# ==========================================\nprint(\"\\n\" + \"=\" * 80)\nprint(\"🔮 開始預測測試集\")\nprint(\"=\" * 80)\n\ntest_df = pd.read_csv(TEST_CSV)\ntest_df['image_id'] = test_df['sample_id'].apply(lambda x: x.split('__')[0])\nunique_test_ids = test_df['image_id'].unique()\n\npredictions_dict = {}\nprint(\"\\n正在預測測試集...\")\n\nfor img_id in tqdm(unique_test_ids, desc=\"測試集預測\"):\n    filename = img_id if img_id.endswith('.jpg') else f\"{img_id}.jpg\"\n    img_path = os.path.join(TEST_IMG_DIR, filename)\n    \n    crops, mask_crops = crop_image_8_tiles_with_mask(img_path, use_mask=True)\n    \n    crop_predictions = []\n    for crop, mask_crop in zip(crops, mask_crops):\n        feat = extract_features_from_crop(crop, mask_crop)\n        if feat is not None:\n            # 多模型預測\n            model_preds = []\n            for name, model in trained_models.items():\n                pred = model.predict([feat])[0]\n                pred = np.clip(pred, 0, None)\n                model_preds.append(pred)\n            \n            # 融合預測\n            ensemble = np.mean(model_preds, axis=0)\n            crop_predictions.append(ensemble)\n    \n    if len(crop_predictions) > 0:\n        predictions_dict[img_id] = np.mean(crop_predictions, axis=0)\n    else:\n        predictions_dict[img_id] = np.zeros(len(target_cols))\n\n# ==========================================\n# 9. 生成提交檔案\n# ==========================================\nprint(\"\\n生成提交檔案...\")\n\nsubmission_data = []\nfor _, row in test_df.iterrows():\n    sample_id = row['sample_id']\n    img_id, target_name = sample_id.split('__')\n    \n    if img_id in predictions_dict:\n        preds = predictions_dict[img_id]\n        target_idx = target_cols.index(target_name)\n        value = preds[target_idx]\n    else:\n        value = 0.0\n    \n    submission_data.append({\n        'sample_id': sample_id,\n        'target': value\n    })\n\nsubmission_df = pd.DataFrame(submission_data)\nsubmission_df.to_csv('submission.csv', index=False)\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"✅ 提交檔案已生成: submission.csv\")\nprint(\"=\" * 80)\nprint(f\"\\n融合模型驗證集表現:\")\nprint(f\"  RMSE: {ensemble_rmse:.4f}\")\nprint(f\"  MAE:  {ensemble_mae:.4f}\")\nprint(f\"  R²:   {ensemble_r2:.4f}\")\nprint(\"\\n改進重點:\")\nif USE_SEGMENTATION:\n    print(\"  ✓ 使用 SegFormer 語義分割精確識別植被\")\nelse:\n    print(\"  ✓ 使用 HSV 顏色空間識別植被\")\nprint(\"  ✓ 去除泥土背景，只保留植被區域\")\nprint(\"  ✓ 支援多種植被顏色（綠、黃、棕）\")\nprint(\"  ✓ 多模型融合提升穩定性\")\nprint(\"  ✓ 加入植被覆蓋率特徵\")\nprint(\"=\" * 80)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-28T10:16:26.294538Z","iopub.execute_input":"2025-12-28T10:16:26.295229Z","iopub.status.idle":"2025-12-28T10:16:53.88759Z","shell.execute_reply.started":"2025-12-28T10:16:26.295201Z","shell.execute_reply":"2025-12-28T10:16:53.886555Z"}},"outputs":[],"execution_count":null}]}