{"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 & Imports (Kaggle-safe: GPU fallback)\n# =========================================================\nimport os\n\n# Reduce TF log spam\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"\n\n# Optional: if you want to FORCE CPU always, set this to \"1\"\nFORCE_CPU = False\nif FORCE_CPU:\n    os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"-1\"\n\nimport ast\nimport time\nimport random\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix\n\n# Reproducibility\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\n# ---- GPU handling (avoid cuInit crash) ----\ndef configure_tf_device():\n    try:\n        gpus = tf.config.list_physical_devices(\"GPU\")\n        if not gpus:\n            print(\"GPU: none detected -> using CPU\")\n            return\n        # If GPU exists, enable memory growth (stability on Kaggle)\n        for gpu in gpus:\n            tf.config.experimental.set_memory_growth(gpu, True)\n        print(\"GPU(s) detected:\", gpus)\n    except Exception as e:\n        # If anything CUDA-related fails, hard fall back to CPU for this session\n        print(\"GPU init failed, falling back to CPU. Error:\", repr(e))\n        os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"-1\"\n\nconfigure_tf_device()\n\nprint(\"TF version:\", tf.__version__)\nprint(\"Visible GPUs:\", tf.config.list_physical_devices(\"GPU\"))\n\n# ---------------------------------------------------------\n# Kaggle dataset path auto-detection\n# ---------------------------------------------------------\nKAGGLE_INPUT_ROOT = \"/kaggle/input\"\nprint(\"\\nAvailable datasets in /kaggle/input:\")\nprint(sorted(os.listdir(KAGGLE_INPUT_ROOT)) if os.path.exists(KAGGLE_INPUT_ROOT) else \"not found\")\n\ndef find_gwd_dir(root=\"/kaggle/input\"):\n    \"\"\"\n    Locate Global Wheat Detection folder that contains train.csv and train/ directory.\n    \"\"\"\n    if not os.path.exists(root):\n        return None\n    for d in sorted(os.listdir(root)):\n        cand = os.path.join(root, d)\n        if os.path.isdir(cand) and os.path.exists(os.path.join(cand, \"train.csv\")) and os.path.isdir(os.path.join(cand, \"train\")):\n            return cand\n    return None\n\nDATA_DIR = find_gwd_dir(KAGGLE_INPUT_ROOT)\nif DATA_DIR is None:\n    raise FileNotFoundError(\n        \"Global Wheat Detection dataset not found in /kaggle/input.\\n\"\n        \"Kaggle Notebook: 오른쪽 'Add Data'에서 competition dataset을 추가하세요.\"\n    )\n\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train\")\nTRAIN_CSV = os.path.join(DATA_DIR, \"train.csv\")\n\nprint(\"\\nUsing DATA_DIR:\", DATA_DIR)\nprint(\"TRAIN_CSV:\", TRAIN_CSV)\nprint(\"TRAIN_IMG_DIR:\", TRAIN_IMG_DIR)\n\nassert os.path.exists(TRAIN_CSV), \"train.csv missing.\"\nassert os.path.exists(TRAIN_IMG_DIR), \"train/ folder missing.\"\n\nOUT_DIR = \"/kaggle/working\"\nos.makedirs(OUT_DIR, exist_ok=True)\nprint(\"\\nOUT_DIR:\", OUT_DIR)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-07T12:56:13.339184Z","iopub.execute_input":"2026-02-07T12:56:13.339477Z","iopub.status.idle":"2026-02-07T12:56:28.558393Z","shell.execute_reply.started":"2026-02-07T12:56:13.339444Z","shell.execute_reply":"2026-02-07T12:56:28.557606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 2 — Load train.csv & Parse Bounding Boxes\n# =========================================================\ndf = pd.read_csv(TRAIN_CSV)\n\nprint(\"train.csv shape:\", df.shape)\nprint(df.head(3))\nprint(\"Unique images:\", df[\"image_id\"].nunique())\n\ndef parse_bbox(bbox_str):\n    # bbox_str format: \"[x, y, w, h]\"\n    return ast.literal_eval(bbox_str)\n\ndf[\"bbox_list\"] = df[\"bbox\"].apply(parse_bbox)\n\n# Map: image_id -> list of bboxes\nbboxes_by_image = {}\n# Map: image_id -> (width, height)\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\nex_id = next(iter(bboxes_by_image.keys()))\nprint(\"\\nExample image_id:\", ex_id)\nprint(\"BBox count:\", len(bboxes_by_image[ex_id]))\nprint(\"Image size:\", size_by_image[ex_id])\nprint(\"First bbox:\", bboxes_by_image[ex_id][0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T12:56:36.178521Z","iopub.execute_input":"2026-02-07T12:56:36.179433Z","iopub.status.idle":"2026-02-07T12:56:38.784329Z","shell.execute_reply.started":"2026-02-07T12:56:36.179403Z","shell.execute_reply":"2026-02-07T12:56:38.783664Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 2.5 — Preview + SAVE Detection Annotations (BBox Overlay Samples)\n#            + ZIP only this preprocessing output\n# =========================================================\nimport os, random, zipfile\nimport matplotlib.pyplot as plt\n\n# Try OpenCV for drawing boxes; fallback to PIL if not available\ntry:\n    import cv2\n    HAS_CV2 = True\nexcept Exception:\n    HAS_CV2 = False\n    from PIL import Image, ImageDraw\n\n# -----------------------------\n# SETTINGS\n# -----------------------------\nN_SAMPLES = 100       # how many overlay images to save\nMAX_BOXES = 300       # max boxes drawn per image\n\nSAVE_DIR = \"/kaggle/working/dataset_preprocessed/detection_overlay\"\nZIP_PATH = \"/kaggle/working/detection_overlay_samples.zip\"\n\nos.makedirs(SAVE_DIR, exist_ok=True)\n\n# pick random image_ids\nall_ids = list(bboxes_by_image.keys())\nrandom.shuffle(all_ids)\nchosen_ids = all_ids[:N_SAMPLES]\n\nprint(\"Saving bbox overlay samples to:\", SAVE_DIR)\nprint(\"Will zip to:\", ZIP_PATH)\nprint(\"Samples:\", len(chosen_ids), \"| Max boxes/image:\", MAX_BOXES)\nprint()\n\ndef draw_and_save_overlay(image_id: str):\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)  # BGR\n        if img is None:\n            return None\n\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\n        cv2.imwrite(out_path, img)\n        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        return out_path, img_rgb\n\n    else:\n        img = Image.open(img_path).convert(\"RGB\")\n        draw = ImageDraw.Draw(img)\n\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\n        img.save(out_path)\n        return out_path, img\n\nsaved_paths = []\npreview_count = 3\n\nfor i, image_id in enumerate(chosen_ids):\n    result = draw_and_save_overlay(image_id)\n    if result is None:\n        print(f\"[SKIP] {image_id} (missing/unreadable)\")\n        continue\n\n    out_path, preview_img = result\n    saved_paths.append(out_path)\n    print(f\"[OK] {os.path.basename(out_path)}\")\n\n    if i < preview_count:\n        plt.figure(figsize=(9, 9))\n        plt.imshow(preview_img)\n        plt.title(f\"BBox Overlay — {image_id}\")\n        plt.axis(\"off\")\n        plt.show()\n\nprint(\"\\nDone saving overlays. Total saved:\", len(saved_paths))\nprint(\"Folder contains:\", len(os.listdir(SAVE_DIR)), \"files\")\n\n# -----------------------------\n# ZIP ONLY THIS FOLDER OUTPUT\n# -----------------------------\n# (Recreate zip each run so it always matches current content)\nif os.path.exists(ZIP_PATH):\n    os.remove(ZIP_PATH)\n\nwith zipfile.ZipFile(ZIP_PATH, \"w\", compression=zipfile.ZIP_DEFLATED) as z:\n    for fn in sorted(os.listdir(SAVE_DIR)):\n        if fn.lower().endswith((\".png\", \".jpg\", \".jpeg\")):\n            full = os.path.join(SAVE_DIR, fn)\n            # keep a nice folder name inside the zip\n            z.write(full, arcname=os.path.join(\"detection_overlay\", fn))\n\nprint(\"\\n✅ ZIP created:\", ZIP_PATH)\nprint(\"Download from Kaggle: Output panel ->\", os.path.basename(ZIP_PATH))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T12:58:21.069705Z","iopub.execute_input":"2026-02-07T12:58:21.070063Z","iopub.status.idle":"2026-02-07T12:58:45.445966Z","shell.execute_reply.started":"2026-02-07T12:58:21.070036Z","shell.execute_reply":"2026-02-07T12:58:45.445282Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 3 — Build Classification Samples (Positive & Negative Crops)\n#         Positive: bbox crop (wheat head)\n#         Negative: random crop with minimal overlap to any bbox\n# =========================================================\ndef iou_xywh(a, b):\n    # a,b: [x,y,w,h]\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\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    \"\"\"\n    Create a negative crop with same (w,h) as ref_box, placed randomly.\n    Reject if overlaps too much with any positive bbox.\n    \"\"\"\n    _, _, w, h = ref_box\n    w = int(max(8, min(w, img_w)))\n    h = int(max(8, min(h, img_h)))\n\n    if w >= img_w or h >= img_h:\n        return None\n\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        if all(iou_xywh(cand, pb) < iou_thresh for pb in pos_boxes):\n            return cand\n    return None\n\nNEG_PER_POS = 1\nIOU_THRESH_NEG = 0.05\n\nsamples = []  # (img_path, box_norm[y1,x1,y2,x2], label)\npos_count, neg_count = 0, 0\n\nfor image_id, pos_boxes in bboxes_by_image.items():\n    img_path = os.path.join(TRAIN_IMG_DIR, f\"{image_id}.jpg\")\n    if not os.path.exists(img_path):\n        continue\n\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, x)\n        y1 = max(0.0, y)\n        x2 = min(float(img_w), x1 + float(w))\n        y2 = min(float(img_h), y1 + float(h))\n\n        # normalized [y1,x1,y2,x2] for crop_and_resize\n        box_norm = [y1 / img_h, x1 / img_w, y2 / img_h, x2 / img_w]\n        samples.append((img_path, box_norm, 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            nx1, ny1 = nx, ny\n            nx2, ny2 = nx1 + nw, ny1 + nh\n            nbox_norm = [ny1 / img_h, nx1 / img_w, ny2 / img_h, nx2 / img_w]\n            samples.append((img_path, nbox_norm, 0))\n            neg_count += 1\n\nprint(\"Positive samples:\", pos_count)\nprint(\"Negative samples:\", neg_count)\nprint(\"Total samples:\", len(samples))\n\npaths = np.array([s[0] for s in samples])\nboxes = np.array([s[1] for s in samples], dtype=np.float32)\nlabels = np.array([s[2] for s in samples], dtype=np.int32)\n\nprint(\"\\nLabel distribution:\", np.bincount(labels))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T06:21:28.613592Z","iopub.execute_input":"2026-02-06T06:21:28.613859Z","iopub.status.idle":"2026-02-06T06:21:53.538303Z","shell.execute_reply.started":"2026-02-06T06:21:28.613837Z","shell.execute_reply":"2026-02-06T06:21:53.537674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 4 — Train/Val/Test Split (Stratified)\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())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T06:21:53.539894Z","iopub.execute_input":"2026-02-06T06:21:53.540321Z","iopub.status.idle":"2026-02-06T06:21:53.775542Z","shell.execute_reply.started":"2026-02-06T06:21:53.540296Z","shell.execute_reply":"2026-02-06T06:21:53.774854Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 5 — TF Dataset + Scenario 2 HSV Masking\n# =========================================================\nIMG_SIZE = 160\nBATCH = 32\nAUTOTUNE = tf.data.AUTOTUNE\n\n# HSV bounds (tf.image.rgb_to_hsv => 0..1)\n# You can tune these!\nHSV_H_MIN, HSV_H_MAX = 0.05, 0.45\nHSV_S_MIN, HSV_S_MAX = 0.20, 1.00\nHSV_V_MIN, HSV_V_MAX = 0.20, 1.00\n\ndef hsv_mask_rgb01(img_rgb01):\n    hsv = tf.image.rgb_to_hsv(img_rgb01)\n    h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]\n    mask = (\n        (h >= HSV_H_MIN) & (h <= HSV_H_MAX) &\n        (s >= HSV_S_MIN) & (s <= HSV_S_MAX) &\n        (v >= HSV_V_MIN) & (v <= HSV_V_MAX)\n    )\n    mask = tf.cast(mask, tf.float32)\n    return img_rgb01 * tf.expand_dims(mask, axis=-1)\n\ndef load_crop_and_preprocess(path, box, label, augment=False):\n    img_bytes = tf.io.read_file(path)\n    img = tf.image.decode_jpeg(img_bytes, channels=3)\n    img = tf.image.convert_image_dtype(img, tf.float32)  # [0..1]\n\n    img_b = tf.expand_dims(img, axis=0)\n    box_b = tf.expand_dims(box, axis=0)\n    idx = tf.zeros([1], dtype=tf.int32)\n\n    crop = tf.image.crop_and_resize(img_b, box_b, idx, crop_size=(IMG_SIZE, IMG_SIZE))[0]\n    crop = hsv_mask_rgb01(crop)\n\n    if augment:\n        crop = tf.image.random_flip_left_right(crop)\n        crop = tf.image.random_flip_up_down(crop)\n        crop = tf.image.random_brightness(crop, max_delta=0.10)\n\n    return crop, tf.cast(label, tf.float32)\n\ndef make_ds(X, B, y, training=False):\n    ds = tf.data.Dataset.from_tensor_slices((X, B, y))\n    if training:\n        ds = ds.shuffle(2048, seed=SEED, reshuffle_each_iteration=True)\n    ds = ds.map(lambda p, b, l: load_crop_and_preprocess(p, b, l, augment=training),\n                num_parallel_calls=AUTOTUNE)\n    return ds.batch(BATCH).prefetch(AUTOTUNE)\n\ntrain_ds = make_ds(X_train, b_train, y_train, training=True)\nval_ds   = make_ds(X_val,   b_val,   y_val,   training=False)\ntest_ds  = make_ds(X_test,  b_test,  y_test,  training=False)\n\nxb, yb = next(iter(train_ds))\nprint(\"Batch X:\", xb.shape, \"Batch y:\", yb.shape)\nprint(\"Pixel range:\", float(tf.reduce_min(xb)), float(tf.reduce_max(xb)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T06:21:53.776533Z","iopub.execute_input":"2026-02-06T06:21:53.776876Z","iopub.status.idle":"2026-02-06T06:21:57.179699Z","shell.execute_reply.started":"2026-02-06T06:21:53.776841Z","shell.execute_reply":"2026-02-06T06:21:57.179004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 6 — Simple CNN Model\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()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T06:21:57.180744Z","iopub.execute_input":"2026-02-06T06:21:57.181367Z","iopub.status.idle":"2026-02-06T06:21:57.996306Z","shell.execute_reply.started":"2026-02-06T06:21:57.181342Z","shell.execute_reply":"2026-02-06T06:21:57.995572Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 7 — Train\n# =========================================================\nEPOCHS = 2\nckpt_path = os.path.join(OUT_DIR, \"cnn_hsv_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\n# Try tqdm-based Keras progress bar\ntry:\n    from tqdm.keras import TqdmCallback\n    callbacks.append(TqdmCallback(verbose=1))\n    fit_verbose = 0  # silence default Keras bar\n    print(\"Using tqdm progress bar for training.\")\nexcept Exception as e:\n    fit_verbose = 1\n    print(\"tqdm.keras not available; using default Keras progress bar. Details:\", repr(e))\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 to:\", ckpt_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T06:21:57.997260Z","iopub.execute_input":"2026-02-06T06:21:57.997547Z","iopub.status.idle":"2026-02-06T09:23:21.982897Z","shell.execute_reply.started":"2026-02-06T06:21:57.997525Z","shell.execute_reply":"2026-02-06T09:23:21.982353Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 8 — Evaluate (Accuracy, Precision, Recall, F1-Score)\n# =========================================================\nbest_model = tf.keras.models.load_model(ckpt_path)\n\ny_prob = best_model.predict(test_ds, verbose=0).reshape(-1)\ny_pred = (y_prob >= 0.5).astype(int)\ny_true = y_test.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\nprint(\"=== TEST METRICS (Scenario 2: HSV + Simple CNN) ===\")\nprint(f\"Accuracy : {acc:.4f}\")\nprint(f\"Precision: {prec:.4f}\")\nprint(f\"Recall   : {rec:.4f}\")\nprint(f\"F1-Score : {f1:.4f}\")\nprint(\"\\nConfusion Matrix [ [TN FP], [FN TP] ]:\\n\", cm)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T09:24:20.193604Z","iopub.execute_input":"2026-02-06T09:24:20.194229Z","iopub.status.idle":"2026-02-06T09:26:57.777389Z","shell.execute_reply.started":"2026-02-06T09:24:20.194200Z","shell.execute_reply":"2026-02-06T09:26:57.776670Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 9 — Visualize Confusion Matrix (False/True labels)\n# =========================================================\nimport matplotlib.pyplot as plt\nimport numpy as np  # make sure this exists somewhere above\n\ntn, fp, fn, tp = cm.ravel()\n\nplt.figure(figsize=(5, 4))\nplt.imshow(cm)\nplt.title(\"Confusion Matrix\")\n\n# Columns = Predicted, Rows = Actual (True labels)\nplt.xticks([0, 1], [\"Pred False\", \"Pred True\"])\nplt.yticks([0, 1], [\"Actual False\", \"Actual True\"])\n\nfor (i, j), val in np.ndenumerate(cm):\n    plt.text(j, i, int(val), ha=\"center\", va=\"center\")\n\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"Actual\")\nplt.show()\n\nprint(f\"TN={tn}, FP={fp}, FN={fn}, TP={tp}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T12:16:02.165217Z","iopub.execute_input":"2026-02-07T12:16:02.165539Z","iopub.status.idle":"2026-02-07T12:16:02.186183Z","shell.execute_reply.started":"2026-02-07T12:16:02.165504Z","shell.execute_reply":"2026-02-07T12:16:02.184934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 10 — Save Metrics to CSV (for report table)\n# =========================================================\nmetrics_df = pd.DataFrame([{\n    \"scenario\": \"HSV_thresholding\",\n    \"model\": \"SimpleCNN\",\n    \"img_size\": IMG_SIZE,\n    \"neg_per_pos\": NEG_PER_POS,\n    \"accuracy\": acc,\n    \"precision\": prec,\n    \"recall\": rec,\n    \"f1_score\": f1,\n    \"seed\": SEED\n}])\n\ncsv_path = os.path.join(OUT_DIR, \"metrics_hsv_simplecnn.csv\")\nmetrics_df.to_csv(csv_path, index=False)\nprint(\"Saved metrics CSV:\", csv_path)\nmetrics_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T09:27:27.094185Z","iopub.execute_input":"2026-02-06T09:27:27.094878Z","iopub.status.idle":"2026-02-06T09:27:27.122998Z","shell.execute_reply.started":"2026-02-06T09:27:27.094853Z","shell.execute_reply":"2026-02-06T09:27:27.122427Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# CELL 11 — (Optional) Save Train History Plot\n# =========================================================\nhist = history.history\n\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()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T09:27:39.802183Z","iopub.execute_input":"2026-02-06T09:27:39.802924Z","iopub.status.idle":"2026-02-06T09:27:40.054578Z","shell.execute_reply.started":"2026-02-06T09:27:39.802896Z","shell.execute_reply":"2026-02-06T09:27:40.053956Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# FINAL CELL — Save artifacts (model + metrics + zip)\n# =========================================================\nimport os, json, shutil, zipfile\nimport pandas as pd\nimport tensorflow as tf\n\nART_DIR = os.path.join(OUT_DIR, \"artifacts_hsv_simplecnn\")\nos.makedirs(ART_DIR, exist_ok=True)\n\n# 1) Save BEST model file (already produced by ModelCheckpoint)\nbest_model_path = os.path.join(OUT_DIR, \"cnn_hsv_best.keras\")\nassert os.path.exists(best_model_path), \"Best model .keras not found. Check training cell / ckpt_path.\"\n\n# Copy into artifacts folder\nshutil.copy2(best_model_path, os.path.join(ART_DIR, \"cnn_hsv_best.keras\"))\n\n# 2) (Optional) Export SavedModel format (folder)\nbest_model = tf.keras.models.load_model(best_model_path)\nsavedmodel_dir = os.path.join(ART_DIR, \"savedmodel\")\n# clean old if exists\nif os.path.exists(savedmodel_dir):\n    shutil.rmtree(savedmodel_dir)\nbest_model.export(savedmodel_dir)  # TF 2.19 supports model.export()\nprint(\"SavedModel exported to:\", savedmodel_dir)\n\n# 3) Save metrics (JSON + CSV)\nmetrics = {\n    \"scenario\": \"HSV_thresholding\",\n    \"model\": \"SimpleCNN\",\n    \"img_size\": int(IMG_SIZE),\n    \"neg_per_pos\": int(NEG_PER_POS),\n    \"seed\": int(SEED),\n    \"accuracy\": float(acc),\n    \"precision\": float(prec),\n    \"recall\": float(rec),\n    \"f1_score\": float(f1),\n}\nwith open(os.path.join(ART_DIR, \"metrics.json\"), \"w\") as f:\n    json.dump(metrics, f, indent=2)\n\nmetrics_df = pd.DataFrame([metrics])\nmetrics_df.to_csv(os.path.join(ART_DIR, \"metrics.csv\"), index=False)\n\n# 4) Save training history\nhist_df = pd.DataFrame(history.history)\nhist_df.to_csv(os.path.join(ART_DIR, \"train_history.csv\"), index=False)\n\n# 5) Zip everything for easy download\nzip_path = os.path.join(OUT_DIR, \"artifacts_hsv_simplecnn.zip\")\nif os.path.exists(zip_path):\n    os.remove(zip_path)\n\nwith zipfile.ZipFile(zip_path, \"w\", compression=zipfile.ZIP_DEFLATED) as z:\n    for root, _, files in os.walk(ART_DIR):\n        for fn in files:\n            full = os.path.join(root, fn)\n            rel = os.path.relpath(full, ART_DIR)\n            z.write(full, arcname=os.path.join(\"artifacts_hsv_simplecnn\", rel))\n\nprint(\"\\n✅ Saved artifacts folder:\", ART_DIR)\nprint(\"✅ Zipped artifacts:\", zip_path)\nprint(\"\\nDownload from Kaggle: 'Output' panel ->\", os.path.basename(zip_path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-06T09:29:15.073279Z","iopub.execute_input":"2026-02-06T09:29:15.073886Z","iopub.status.idle":"2026-02-06T09:29:15.748160Z","shell.execute_reply.started":"2026-02-06T09:29:15.073857Z","shell.execute_reply":"2026-02-06T09:29:15.747597Z"}},"outputs":[],"execution_count":null}]}