{"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":19989,"databundleVersionId":1160143,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================================================\n# CELL 1 — Setup\n# =========================================================\nimport os, random, math\nimport numpy as np\nimport pandas as pd\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\nDATA_DIR = \"/kaggle/input/global-wheat-detection\"\nTRAIN_CSV = os.path.join(DATA_DIR, \"train.csv\")\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train\")\nOUT_DIR = \"/kaggle/working\"\n\nprint(\"DATA_DIR:\", DATA_DIR)\nprint(\"TRAIN_CSV exists:\", os.path.exists(TRAIN_CSV))\nprint(\"TRAIN_IMG_DIR exists:\", os.path.exists(TRAIN_IMG_DIR))\nprint(\"OUT_DIR:\", OUT_DIR)\n\nassert os.path.exists(TRAIN_CSV), \"train.csv not found. Add competition data.\"\nassert os.path.exists(TRAIN_IMG_DIR), \"train images folder not found.\"","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-13T12:56:32.400005Z","iopub.execute_input":"2026-02-13T12:56:32.400572Z","iopub.status.idle":"2026-02-13T12:56:32.688036Z","shell.execute_reply.started":"2026-02-13T12:56:32.400543Z","shell.execute_reply":"2026-02-13T12:56:32.687462Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 2 — Load train.csv + bbox map\n# =========================================================\nimport ast\n\ndf = pd.read_csv(TRAIN_CSV)\nprint(\"train.csv shape:\", df.shape)\nprint(df.head())\n\ndef parse_bbox(bbox_str):\n    return ast.literal_eval(bbox_str)  # [x,y,w,h]\n\ndf[\"bbox_list\"] = df[\"bbox\"].apply(parse_bbox)\n\nbboxes_by_image = {}\nsize_by_image = {}\n\nfor row in df.itertuples(index=False):\n    image_id = row.image_id\n    x, y, w, h = row.bbox_list\n    bboxes_by_image.setdefault(image_id, []).append([float(x), float(y), float(w), float(h)])\n    size_by_image[image_id] = (int(row.width), int(row.height))\n\nprint(\"Unique images:\", len(bboxes_by_image))\nex_id = next(iter(bboxes_by_image.keys()))\nprint(\"Example:\", ex_id, \"bbox_count:\", len(bboxes_by_image[ex_id]), \"size:\", size_by_image[ex_id])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T12:56:32.689263Z","iopub.execute_input":"2026-02-13T12:56:32.689583Z","iopub.status.idle":"2026-02-13T12:56:35.169553Z","shell.execute_reply.started":"2026-02-13T12:56:32.689560Z","shell.execute_reply":"2026-02-13T12:56:35.168742Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 2.5 — SAVE Detection BBox Overlay Samples (evidence)\n# =========================================================\nimport matplotlib.pyplot as plt\n\ntry:\n    import cv2\n    HAS_CV2 = True\nexcept Exception:\n    HAS_CV2 = False\n    from PIL import Image, ImageDraw\n\nN_SAMPLES = 20\nMAX_BOXES = 80\nSAVE_DIR = \"/kaggle/working/dataset_preprocessed_s3/detection_overlay\"\nos.makedirs(SAVE_DIR, exist_ok=True)\n\nall_ids = list(bboxes_by_image.keys())\nrandom.shuffle(all_ids)\nchosen_ids = all_ids[:N_SAMPLES]\n\ndef draw_and_save_overlay(image_id):\n    img_path = os.path.join(TRAIN_IMG_DIR, f\"{image_id}.jpg\")\n    if not os.path.exists(img_path):\n        return None\n\n    bboxes = bboxes_by_image.get(image_id, [])\n    out_path = os.path.join(SAVE_DIR, f\"bbox_overlay_{image_id}.png\")\n\n    if HAS_CV2:\n        img = cv2.imread(img_path)\n        if img is None:\n            return None\n        for (x, y, w, h) in bboxes[:MAX_BOXES]:\n            x, y, w, h = int(x), int(y), int(w), int(h)\n            cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 255), 2)\n        cv2.imwrite(out_path, img)\n        return out_path, cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    else:\n        img = Image.open(img_path).convert(\"RGB\")\n        draw = ImageDraw.Draw(img)\n        for (x, y, w, h) in bboxes[:MAX_BOXES]:\n            x, y, w, h = int(x), int(y), int(w), int(h)\n            draw.rectangle([x, y, x+w, y+h], outline=(255, 255, 0), width=3)\n        img.save(out_path)\n        return out_path, img\n\nsaved = []\nfor i, image_id in enumerate(chosen_ids):\n    res = draw_and_save_overlay(image_id)\n    if res is None:\n        continue\n    out_path, preview = res\n    saved.append(out_path)\n    if i < 3:\n        plt.figure(figsize=(9,9))\n        plt.imshow(preview)\n        plt.title(f\"BBox Overlay — {image_id}\")\n        plt.axis(\"off\")\n        plt.show()\n\nprint(\"Saved overlays:\", len(saved))\nprint(\"Overlay folder:\", SAVE_DIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T12:56:35.170471Z","iopub.execute_input":"2026-02-13T12:56:35.170720Z","iopub.status.idle":"2026-02-13T12:56:37.995716Z","shell.execute_reply.started":"2026-02-13T12:56:35.170693Z","shell.execute_reply":"2026-02-13T12:56:37.994823Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 3 — Build samples (Positive/Negative crops)\n# =========================================================\nfrom tqdm.auto import tqdm\n\ndef iou_xywh(a, b):\n    ax1, ay1, aw, ah = a\n    bx1, by1, bw, bh = b\n    ax2, ay2 = ax1 + aw, ay1 + ah\n    bx2, by2 = bx1 + bw, by1 + bh\n    ix1, iy1 = max(ax1, bx1), max(ay1, by1)\n    ix2, iy2 = min(ax2, bx2), min(ay2, by2)\n    iw, ih = max(0.0, ix2 - ix1), max(0.0, iy2 - iy1)\n    inter = iw * ih\n    union = (aw*ah) + (bw*bh) - inter\n    return inter/union if union > 0 else 0.0\n\ndef make_negative_box(img_w, img_h, pos_boxes, ref_box, max_tries=80, iou_thresh=0.05):\n    _, _, w, h = ref_box\n    w = int(max(8, min(float(w), float(img_w))))\n    h = int(max(8, min(float(h), float(img_h))))\n    if w >= img_w or h >= img_h:\n        return None\n    for _ in range(max_tries):\n        x = random.randint(0, img_w - w)\n        y = random.randint(0, img_h - h)\n        cand = [float(x), float(y), float(w), float(h)]\n        ok = True\n        for pb in pos_boxes:\n            if iou_xywh(cand, pb) >= iou_thresh:\n                ok = False\n                break\n        if ok:\n            return cand\n    return None\n\nNEG_PER_POS = 1\nIOU_THRESH_NEG = 0.05\n\nsamples = []  # (img_path, box_xyxy_pixels, label)\npos_count, neg_count = 0, 0\n\nitems = list(bboxes_by_image.items())\nfor image_id, pos_boxes in tqdm(items, total=len(items), desc=\"Building crop samples\"):\n    img_path = os.path.join(TRAIN_IMG_DIR, f\"{image_id}.jpg\")\n    if not os.path.exists(img_path):\n        continue\n    img_w, img_h = size_by_image[image_id]\n\n    for pb in pos_boxes:\n        x, y, w, h = pb\n        x1 = max(0.0, float(x))\n        y1 = max(0.0, float(y))\n        x2 = min(float(img_w), x1 + float(w))\n        y2 = min(float(img_h), y1 + float(h))\n        if (x2-x1) < 2 or (y2-y1) < 2:\n            continue\n\n        samples.append((img_path, (x1,y1,x2,y2), 1))\n        pos_count += 1\n\n        for _ in range(NEG_PER_POS):\n            nb = make_negative_box(img_w, img_h, pos_boxes, pb, iou_thresh=IOU_THRESH_NEG)\n            if nb is None:\n                continue\n            nx, ny, nw, nh = nb\n            samples.append((img_path, (nx, ny, nx+nw, ny+nh), 0))\n            neg_count += 1\n\nprint(\"Positive:\", pos_count, \"| Negative:\", neg_count, \"| Total:\", len(samples))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T12:56:37.997293Z","iopub.execute_input":"2026-02-13T12:56:37.997575Z","iopub.status.idle":"2026-02-13T12:57:02.887171Z","shell.execute_reply.started":"2026-02-13T12:56:37.997552Z","shell.execute_reply":"2026-02-13T12:57:02.886485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 4 — Split\n# =========================================================\nfrom sklearn.model_selection import train_test_split\n\npaths = np.array([s[0] for s in samples])\nboxes = np.array([s[1] for s in samples], dtype=np.float32)  # x1,y1,x2,y2 pixel\nlabels = np.array([s[2] for s in samples], dtype=np.int32)\n\nX_train, X_tmp, b_train, b_tmp, y_train, y_tmp = train_test_split(\n    paths, boxes, labels, test_size=0.30, random_state=SEED, stratify=labels\n)\nX_val, X_test, b_val, b_test, y_val, y_test = train_test_split(\n    X_tmp, b_tmp, y_tmp, test_size=0.50, random_state=SEED, stratify=y_tmp\n)\n\nprint(\"Train:\", len(X_train), \"Val:\", len(X_val), \"Test:\", len(X_test))\nprint(\"Train pos%:\", y_train.mean(), \"Val pos%:\", y_val.mean(), \"Test pos%:\", y_test.mean())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T12:57:02.888038Z","iopub.execute_input":"2026-02-13T12:57:02.888239Z","iopub.status.idle":"2026-02-13T12:57:04.137312Z","shell.execute_reply.started":"2026-02-13T12:57:02.888221Z","shell.execute_reply":"2026-02-13T12:57:04.136586Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 5 — Install RemBG (Zero-shot segmentation, inference only)\n# =========================================================\n!pip -q install rembg onnxruntime pillow tqdm\n\nfrom rembg import remove, new_session\nfrom PIL import Image\nimport io\n\n# pilih model yang lebih ringan agar cepat (u2netp)\nREMBG_MODEL = \"u2netp\"\nsession = new_session(REMBG_MODEL)\nprint(\"RemBG session loaded:\", REMBG_MODEL)\n\nIMG_SIZE = 160","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T12:57:22.899156Z","iopub.execute_input":"2026-02-13T12:57:22.899759Z","iopub.status.idle":"2026-02-13T12:58:41.662700Z","shell.execute_reply.started":"2026-02-13T12:57:22.899727Z","shell.execute_reply":"2026-02-13T12:58:41.661946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 5.5 — Generate preprocessing output folders (Raw crop vs RemBG crop)\n# =========================================================\nfrom tqdm.auto import tqdm\n\nSAVE_ROOT = \"/kaggle/working/dataset_preprocessed_s3\"\nRAW_DIR   = os.path.join(SAVE_ROOT, \"raw_crops\")\nAI_DIR    = os.path.join(SAVE_ROOT, \"rembg_crops\")  # hasil zero-shot\nos.makedirs(RAW_DIR, exist_ok=True)\nos.makedirs(AI_DIR,  exist_ok=True)\n\ndef pil_crop(img_pil, box_xyxy):\n    x1,y1,x2,y2 = box_xyxy\n    return img_pil.crop((int(x1), int(y1), int(x2), int(y2)))\n\ndef tight_crop_from_alpha(rgba_img, alpha_thresh=10):\n    # rgba_img: PIL Image RGBA\n    arr = np.array(rgba_img)\n    alpha = arr[..., 3]\n    ys, xs = np.where(alpha > alpha_thresh)\n    if len(xs) == 0 or len(ys) == 0:\n        return rgba_img  # fallback\n    x1, x2 = xs.min(), xs.max()\n    y1, y2 = ys.min(), ys.max()\n    # +1 so right/bottom included\n    return rgba_img.crop((x1, y1, x2+1, y2+1))\n\ndef rembg_process_crop(crop_rgb_pil):\n    # input: PIL RGB crop\n    buf = io.BytesIO()\n    crop_rgb_pil.save(buf, format=\"PNG\")\n    inp = buf.getvalue()\n\n    out_bytes = remove(inp, session=session)  # returns PNG bytes, usually with alpha\n    out_img = Image.open(io.BytesIO(out_bytes)).convert(\"RGBA\")\n\n    # auto-crop from alpha mask (crop otomatis dari model AI)\n    out_img = tight_crop_from_alpha(out_img)\n\n    # composite to RGB (black bg) for CNN\n    bg = Image.new(\"RGB\", out_img.size, (0,0,0))\n    bg.paste(out_img, mask=out_img.split()[-1])  # alpha\n    return bg\n\ndef save_preprocessed_set(X_paths, X_boxes, y, split_name, limit=None):\n    \"\"\"\n    Save:\n      raw crop -> RAW_DIR/split/label/\n      rembg crop -> AI_DIR/split/label/\n    \"\"\"\n    split_raw = os.path.join(RAW_DIR, split_name)\n    split_ai  = os.path.join(AI_DIR, split_name)\n    for lab in [0,1]:\n        os.makedirs(os.path.join(split_raw, str(lab)), exist_ok=True)\n        os.makedirs(os.path.join(split_ai,  str(lab)), exist_ok=True)\n\n    idxs = np.arange(len(X_paths))\n    if limit is not None:\n        idxs = idxs[:limit]\n\n    out_paths = []  # final AI paths for training\n    out_labels = []\n\n    for i in tqdm(idxs, desc=f\"Preprocessing {split_name} (RemBG)\"):\n        img_path = X_paths[i]\n        box = X_boxes[i]\n        lab = int(y[i])\n\n        img = Image.open(img_path).convert(\"RGB\")\n        raw_crop = pil_crop(img, box)\n\n        # resize raw for saving (optional)\n        raw_small = raw_crop.resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR)\n\n        # rembg (zero-shot)\n        ai_crop = rembg_process_crop(raw_crop)\n        ai_small = ai_crop.resize((IMG_SIZE, IMG_SIZE), Image.BILINEAR)\n\n        base = os.path.splitext(os.path.basename(img_path))[0]\n        fn = f\"{base}_{i}_{lab}.png\"\n\n        raw_out = os.path.join(split_raw, str(lab), fn)\n        ai_out  = os.path.join(split_ai,  str(lab), fn)\n\n        raw_small.save(raw_out)\n        ai_small.save(ai_out)\n\n        out_paths.append(ai_out)\n        out_labels.append(lab)\n\n    return np.array(out_paths), np.array(out_labels, dtype=np.int32)\n\n# OPTIONAL: kalau takut lama, batasi dulu untuk uji cepat\n# TRAIN_LIMIT = 30000\n# VAL_LIMIT   = 6000\n# TEST_LIMIT  = 6000\nTRAIN_LIMIT = 600\nVAL_LIMIT   = 300\nTEST_LIMIT  = 300\n\ntrain_ai_paths, train_ai_labels = save_preprocessed_set(X_train, b_train, y_train, \"train\", limit=TRAIN_LIMIT)\nval_ai_paths,   val_ai_labels   = save_preprocessed_set(X_val,   b_val,   y_val,   \"val\",   limit=VAL_LIMIT)\ntest_ai_paths,  test_ai_labels  = save_preprocessed_set(X_test,  b_test,  y_test,  \"test\",  limit=TEST_LIMIT)\n\nprint(\"Saved preprocessing folder:\", SAVE_ROOT)\nprint(\"Example RemBG file:\", train_ai_paths[0] if len(train_ai_paths)>0 else None)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:02:00.379237Z","iopub.execute_input":"2026-02-13T13:02:00.379574Z","iopub.status.idle":"2026-02-13T13:08:06.628713Z","shell.execute_reply.started":"2026-02-13T13:02:00.379533Z","shell.execute_reply":"2026-02-13T13:08:06.627874Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 5.6 — Preview preprocessing results\n# =========================================================\nimport matplotlib.pyplot as plt\n\ndef show_pair(split=\"train\", lab=1, n=3):\n    raw_dir = os.path.join(RAW_DIR, split, str(lab))\n    ai_dir  = os.path.join(AI_DIR,  split, str(lab))\n    fns = sorted(os.listdir(ai_dir))[:n]\n    for fn in fns:\n        raw_img = Image.open(os.path.join(raw_dir, fn)).convert(\"RGB\")\n        ai_img  = Image.open(os.path.join(ai_dir, fn)).convert(\"RGB\")\n\n        plt.figure(figsize=(8,4))\n        plt.subplot(1,2,1); plt.imshow(raw_img); plt.title(\"Raw crop\"); plt.axis(\"off\")\n        plt.subplot(1,2,2); plt.imshow(ai_img);  plt.title(\"RemBG (zero-shot)\"); plt.axis(\"off\")\n        plt.tight_layout()\n        plt.show()\n\nshow_pair(split=\"train\", lab=1, n=3)\nshow_pair(split=\"train\", lab=0, n=3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:08:09.923074Z","iopub.execute_input":"2026-02-13T13:08:09.923374Z","iopub.status.idle":"2026-02-13T13:08:10.987978Z","shell.execute_reply.started":"2026-02-13T13:08:09.923346Z","shell.execute_reply":"2026-02-13T13:08:10.987412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 6 — TF Dataset\n# =========================================================\nimport tensorflow as tf\n\nBATCH = 32\nAUTOTUNE = tf.data.AUTOTUNE\n\ndef load_png(path, label):\n    img_bytes = tf.io.read_file(path)\n    img = tf.image.decode_png(img_bytes, channels=3)\n    img = tf.image.convert_image_dtype(img, tf.float32)  # [0..1]\n    return img, tf.cast(label, tf.float32)\n\ndef make_ds(paths, labels, training=False):\n    ds = tf.data.Dataset.from_tensor_slices((paths, labels))\n    if training:\n        ds = ds.shuffle(2048, seed=SEED, reshuffle_each_iteration=True)\n    ds = ds.map(load_png, num_parallel_calls=AUTOTUNE)\n    return ds.batch(BATCH).prefetch(AUTOTUNE)\n\ntrain_ds = make_ds(train_ai_paths, train_ai_labels, training=True)\nval_ds   = make_ds(val_ai_paths,   val_ai_labels,   training=False)\ntest_ds  = make_ds(test_ai_paths,  test_ai_labels,  training=False)\n\nxb, yb = next(iter(train_ds))\nprint(\"Batch:\", xb.shape, yb.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:08:22.654174Z","iopub.execute_input":"2026-02-13T13:08:22.654900Z","iopub.status.idle":"2026-02-13T13:08:38.394428Z","shell.execute_reply.started":"2026-02-13T13:08:22.654871Z","shell.execute_reply":"2026-02-13T13:08:38.393433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 7 — Simple CNN\n# =========================================================\nfrom tensorflow.keras import layers, models\n\ndef build_simple_cnn(input_shape=(IMG_SIZE, IMG_SIZE, 3)):\n    inputs = layers.Input(shape=input_shape)\n    x = layers.Conv2D(32, 3, padding=\"same\", activation=\"relu\")(inputs)\n    x = layers.MaxPool2D()(x)\n    x = layers.Conv2D(64, 3, padding=\"same\", activation=\"relu\")(x)\n    x = layers.MaxPool2D()(x)\n    x = layers.Conv2D(128, 3, padding=\"same\", activation=\"relu\")(x)\n    x = layers.MaxPool2D()(x)\n    x = layers.Conv2D(128, 3, padding=\"same\", activation=\"relu\")(x)\n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dropout(0.30)(x)\n    outputs = layers.Dense(1, activation=\"sigmoid\")(x)\n    return models.Model(inputs, outputs)\n\nmodel = build_simple_cnn()\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-3),\n    loss=\"binary_crossentropy\",\n    metrics=[\n        tf.keras.metrics.BinaryAccuracy(name=\"acc\", threshold=0.5),\n        tf.keras.metrics.Precision(name=\"precision\", thresholds=0.5),\n        tf.keras.metrics.Recall(name=\"recall\", thresholds=0.5),\n    ],\n)\n\nmodel.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:08:46.843794Z","iopub.execute_input":"2026-02-13T13:08:46.844642Z","iopub.status.idle":"2026-02-13T13:08:47.649978Z","shell.execute_reply.started":"2026-02-13T13:08:46.844613Z","shell.execute_reply":"2026-02-13T13:08:47.649406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 8 — Train (EPOCHS fixed = 2)\n# =========================================================\nEPOCHS = 2\nckpt_path = os.path.join(OUT_DIR, \"cnn_rembg_best.keras\")\n\ncallbacks = [\n    tf.keras.callbacks.ModelCheckpoint(ckpt_path, monitor=\"val_loss\", save_best_only=True, verbose=0),\n    tf.keras.callbacks.EarlyStopping(monitor=\"val_loss\", patience=3, restore_best_weights=True, verbose=1),\n]\n\ntry:\n    from tqdm.keras import TqdmCallback\n    callbacks.append(TqdmCallback(verbose=1))\n    fit_verbose = 0\nexcept Exception:\n    fit_verbose = 1\n\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCHS,\n    callbacks=callbacks,\n    verbose=fit_verbose\n)\n\nprint(\"Saved best model:\", ckpt_path)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:08:52.205254Z","iopub.execute_input":"2026-02-13T13:08:52.205550Z","iopub.status.idle":"2026-02-13T13:09:04.388502Z","shell.execute_reply.started":"2026-02-13T13:08:52.205524Z","shell.execute_reply":"2026-02-13T13:09:04.387821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 9 — Evaluate\n# =========================================================\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix\n\nbest_model = tf.keras.models.load_model(ckpt_path)\n\ny_prob = best_model.predict(test_ds, verbose=1).reshape(-1)\ny_pred = (y_prob >= 0.5).astype(int)\ny_true = test_ai_labels.astype(int)\n\nacc  = accuracy_score(y_true, y_pred)\nprec = precision_score(y_true, y_pred, zero_division=0)\nrec  = recall_score(y_true, y_pred, zero_division=0)\nf1   = f1_score(y_true, y_pred, zero_division=0)\ncm   = confusion_matrix(y_true, y_pred)\n\ntn, fp, fn, tp = cm.ravel()\nspec = tn / (tn + fp) if (tn + fp) > 0 else 0.0\n\nprint(\"=== TEST METRICS (Scenario 3: RemBG Zero-shot + Simple CNN) ===\")\nprint(f\"Accuracy : {acc:.4f}\")\nprint(f\"Precision: {prec:.4f}\")\nprint(f\"Recall   : {rec:.4f}\")\nprint(f\"Specificity: {spec:.4f}\")\nprint(f\"F1-Score : {f1:.4f}\")\nprint(\"\\nConfusion Matrix [[TN FP],[FN TP]]:\\n\", cm)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:09:13.441845Z","iopub.execute_input":"2026-02-13T13:09:13.442467Z","iopub.status.idle":"2026-02-13T13:09:14.366328Z","shell.execute_reply.started":"2026-02-13T13:09:13.442439Z","shell.execute_reply":"2026-02-13T13:09:14.365749Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 10 — Visualizations\n# =========================================================\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nplt.figure(figsize=(5,4))\nplt.imshow(cm)\nplt.title(\"Confusion Matrix\")\nplt.xticks([0,1], [\"Pred 0\", \"Pred 1\"])\nplt.yticks([0,1], [\"True 0\", \"True 1\"])\nfor (i,j), val in np.ndenumerate(cm):\n    plt.text(j, i, int(val), ha=\"center\", va=\"center\")\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.show()\n\nhist = history.history\nplt.figure(figsize=(6,4))\nplt.plot(hist.get(\"loss\", []), label=\"train_loss\")\nplt.plot(hist.get(\"val_loss\", []), label=\"val_loss\")\nplt.title(\"Loss Curve\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.show()\n\nplt.figure(figsize=(6,4))\nplt.plot(hist.get(\"acc\", []), label=\"train_acc\")\nplt.plot(hist.get(\"val_acc\", []), label=\"val_acc\")\nplt.title(\"Accuracy Curve\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:09:21.677906Z","iopub.execute_input":"2026-02-13T13:09:21.678543Z","iopub.status.idle":"2026-02-13T13:09:21.993592Z","shell.execute_reply.started":"2026-02-13T13:09:21.678514Z","shell.execute_reply":"2026-02-13T13:09:21.992916Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 11 — Save metrics\n# =========================================================\nimport pandas as pd\n\nmetrics_df = pd.DataFrame([{\n    \"scenario\": \"ZeroShot_RemBG_u2netp\",\n    \"model\": \"SimpleCNN\",\n    \"img_size\": IMG_SIZE,\n    \"neg_per_pos\": NEG_PER_POS,\n    \"iou_thresh_neg\": IOU_THRESH_NEG,\n    \"epochs\": EPOCHS,\n    \"accuracy\": acc,\n    \"precision\": prec,\n    \"recall\": rec,\n    \"specificity\": spec,\n    \"f1_score\": f1,\n    \"seed\": SEED\n}])\n\ncsv_path = os.path.join(OUT_DIR, \"metrics_s3_rembg_simplecnn.csv\")\nmetrics_df.to_csv(csv_path, index=False)\n\nprint(\"Saved:\", csv_path)\nmetrics_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-13T13:09:46.493023Z","iopub.execute_input":"2026-02-13T13:09:46.493599Z","iopub.status.idle":"2026-02-13T13:09:46.520497Z","shell.execute_reply.started":"2026-02-13T13:09:46.493569Z","shell.execute_reply":"2026-02-13T13:09:46.519817Z"}},"outputs":[],"execution_count":null}]}