{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":113558,"databundleVersionId":14456136,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:28:20.146877Z","iopub.execute_input":"2025-11-29T09:28:20.147439Z","iopub.status.idle":"2025-11-29T09:28:26.79707Z","shell.execute_reply.started":"2025-11-29T09:28:20.147412Z","shell.execute_reply":"2025-11-29T09:28:26.796308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nimport cv2\nfrom sklearn.model_selection import train_test_split\nimport matplotlib.pyplot as plt\n\n# ==========================\n# CONFIG\n# ==========================\n\n# Check the exact dataset name in /kaggle/input/ and update if needed\nDATA_DIR = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\n\nIMG_SIZE = (512, 512)   # you can switch to (256, 256) if OOM\nBATCH_SIZE = 4\nEPOCHS = 40\n\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train_images\")\nTRAIN_MASK_DIR = os.path.join(DATA_DIR, \"train_masks\")\n\nSUPP_IMG_DIR = os.path.join(DATA_DIR, \"supplemental_images\")\nSUPP_MASK_DIR = os.path.join(DATA_DIR, \"supplemental_masks\")\n\nTEST_IMG_DIR = os.path.join(DATA_DIR, \"test_images\")\nSAMPLE_SUB_PATH = os.path.join(DATA_DIR, \"sample_submission.csv\")\n\nVALID_EXTS = [\".png\", \".jpg\", \".jpeg\", \".tif\", \".tiff\", \".bmp\"]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:28:38.978194Z","iopub.execute_input":"2025-11-29T09:28:38.97875Z","iopub.status.idle":"2025-11-29T09:28:38.984305Z","shell.execute_reply.started":"2025-11-29T09:28:38.978722Z","shell.execute_reply":"2025-11-29T09:28:38.983461Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_case_id(path):\n    base = os.path.basename(path)\n    case_id, _ = os.path.splitext(base)\n    return case_id\n\ndef build_df_for_split(img_dir, mask_dir, source_name):\n    \"\"\"\n    Build a dataframe with only valid, readable image files.\n    \"\"\"\n    all_files = sorted(glob.glob(os.path.join(img_dir, \"*\")))\n    rows = []\n    bad_files = []\n\n    for img_path in all_files:\n        ext = os.path.splitext(img_path)[1].lower()\n        if ext not in VALID_EXTS:\n            continue  # skip non-image files (e.g., .txt, hidden)\n\n        img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n        if img is None:\n            bad_files.append(img_path)\n            continue\n\n        case_id = get_case_id(img_path)\n\n        # all masks starting with this case_id (0, 1, or many)\n        mask_paths = sorted(glob.glob(os.path.join(mask_dir, case_id + \"*\")))\n        has_forgery = len(mask_paths) > 0\n\n        rows.append({\n            \"case_id\": case_id,\n            \"image_path\": img_path,\n            \"mask_paths\": mask_paths,\n            \"has_forgery\": int(has_forgery),\n            \"source\": source_name,\n        })\n\n    print(f\"[{source_name}] usable images: {len(rows)}, unreadable: {len(bad_files)}\")\n    return pd.DataFrame(rows)\n\ntrain_df_main = build_df_for_split(TRAIN_IMG_DIR, TRAIN_MASK_DIR, \"train\")\ntrain_df_supp = build_df_for_split(SUPP_IMG_DIR, SUPP_MASK_DIR, \"supplemental\")\n\ntrain_df = pd.concat([train_df_main, train_df_supp], ignore_index=True)\nprint(\"Total usable training images:\", len(train_df))\ntrain_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:28:53.967274Z","iopub.execute_input":"2025-11-29T09:28:53.967869Z","iopub.status.idle":"2025-11-29T09:28:58.866364Z","shell.execute_reply.started":"2025-11-29T09:28:53.967842Z","shell.execute_reply":"2025-11-29T09:28:58.865714Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============ mask utils ============\n\ndef load_union_mask(mask_paths, img_size=IMG_SIZE):\n    \"\"\"\n    Given a list of mask paths, read them, resize, and compute the union.\n    If list is empty, returns an all-zero mask.\n    \"\"\"\n    h, w = img_size\n    if len(mask_paths) == 0:\n        return np.zeros((h, w, 1), dtype=np.float32)\n\n    union_mask = np.zeros((h, w), dtype=np.float32)\n\n    for mp in mask_paths:\n        m = cv2.imread(mp, cv2.IMREAD_GRAYSCALE)\n        if m is None:\n            continue\n        m = cv2.resize(m, (w, h), interpolation=cv2.INTER_NEAREST)\n        m = (m > 0).astype(np.float32)\n        union_mask = np.maximum(union_mask, m)\n\n    return union_mask[..., None].astype(np.float32)\n\n# map from case_id -> list of mask paths\ncase_to_masks = {row.case_id: row.mask_paths for row in train_df.itertuples()}\n\ndef _tensor_to_str(x):\n    if isinstance(x, tf.Tensor):\n        x = x.numpy()\n    if isinstance(x, bytes):\n        return x.decode(\"utf-8\")\n    return str(x)\n\ndef py_load_image_and_mask(case_id, image_path):\n    \"\"\"\n    Python function called via tf.py_function.\n    Converts tensors -> strings, then loads image & union mask.\n    \"\"\"\n    case_id = _tensor_to_str(case_id)\n    image_path = _tensor_to_str(image_path)\n\n    # load image\n    img = cv2.imread(image_path, cv2.IMREAD_COLOR)\n    if img is None:\n        h, w = IMG_SIZE\n        img = np.zeros((h, w, 3), dtype=np.float32)\n    else:\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, IMG_SIZE[::-1])\n        img = img.astype(np.float32) / 255.0\n\n    # load union mask\n    mask_paths = case_to_masks.get(case_id, [])\n    mask = load_union_mask(mask_paths, IMG_SIZE)\n\n    return img, mask\n\ndef tf_load_image_and_mask(case_id, image_path):\n    img, mask = tf.py_function(\n        func=py_load_image_and_mask,\n        inp=[case_id, image_path],\n        Tout=[tf.float32, tf.float32],\n    )\n    img.set_shape(IMG_SIZE + (3,))\n    mask.set_shape(IMG_SIZE + (1,))\n    return img, mask\n\ndef augment(img, mask):\n    # simple spatial augment\n    if tf.random.uniform(()) > 0.5:\n        img = tf.image.flip_left_right(img)\n        mask = tf.image.flip_left_right(mask)\n    if tf.random.uniform(()) > 0.5:\n        img = tf.image.flip_up_down(img)\n        mask = tf.image.flip_up_down(mask)\n    return img, mask\n\ndef make_dataset(df, batch_size=BATCH_SIZE, augment_data=False):\n    case_ids = df[\"case_id\"].values.astype(\"str\")\n    img_paths = df[\"image_path\"].values.astype(\"str\")\n\n    ds = tf.data.Dataset.from_tensor_slices((case_ids, img_paths))\n    ds = ds.map(tf_load_image_and_mask, num_parallel_calls=tf.data.AUTOTUNE)\n    if augment_data:\n        ds = ds.map(augment, num_parallel_calls=tf.data.AUTOTUNE)\n    ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    return ds\n\n# train/val split\ntrain_idx, val_idx = train_test_split(\n    np.arange(len(train_df)),\n    test_size=0.2,\n    stratify=train_df[\"has_forgery\"],\n    random_state=42\n)\n\ntrain_df_split = train_df.iloc[train_idx].reset_index(drop=True)\nval_df_split   = train_df.iloc[val_idx].reset_index(drop=True)\n\ntrain_ds = make_dataset(train_df_split, augment_data=True)\nval_ds   = make_dataset(val_df_split,   augment_data=False)\n\nprint(\"Train batches:\", len(train_ds), \"Val batches:\", len(val_ds))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:29:07.242526Z","iopub.execute_input":"2025-11-29T09:29:07.243287Z","iopub.status.idle":"2025-11-29T09:29:08.089308Z","shell.execute_reply.started":"2025-11-29T09:29:07.243261Z","shell.execute_reply":"2025-11-29T09:29:08.088518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def conv_block(x, filters):\n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n    x = layers.Conv2D(filters, 3, padding=\"same\")(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.ReLU()(x)\n    return x\n\ndef build_unet(img_size=IMG_SIZE):\n    inputs = keras.Input(shape=img_size + (3,))\n\n    # Encoder\n    c1 = conv_block(inputs, 32)\n    p1 = layers.MaxPooling2D()(c1)\n\n    c2 = conv_block(p1, 64)\n    p2 = layers.MaxPooling2D()(c2)\n\n    c3 = conv_block(p2, 128)\n    p3 = layers.MaxPooling2D()(c3)\n\n    c4 = conv_block(p3, 256)\n    p4 = layers.MaxPooling2D()(c4)\n\n    # Bottleneck\n    bn = conv_block(p4, 512)\n\n    # Decoder\n    u4 = layers.UpSampling2D()(bn)\n    u4 = layers.Concatenate()([u4, c4])\n    c5 = conv_block(u4, 256)\n\n    u3 = layers.UpSampling2D()(c5)\n    u3 = layers.Concatenate()([u3, c3])\n    c6 = conv_block(u3, 128)\n\n    u2 = layers.UpSampling2D()(c6)\n    u2 = layers.Concatenate()([u2, c2])\n    c7 = conv_block(u2, 64)\n\n    u1 = layers.UpSampling2D()(c7)\n    u1 = layers.Concatenate()([u1, c1])\n    c8 = conv_block(u1, 32)\n\n    outputs = layers.Conv2D(1, 1, activation=\"sigmoid\")(c8)\n\n    return keras.Model(inputs, outputs, name=\"unet_baseline\")\n\ndef dice_coef(y_true, y_pred, smooth=1e-6):\n    y_true = tf.reshape(y_true, [-1])\n    y_pred = tf.reshape(y_pred, [-1])\n    intersection = tf.reduce_sum(y_true * y_pred)\n    return (2. * intersection + smooth) / (\n        tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) + smooth\n    )\n\ndef bce_dice_loss(y_true, y_pred):\n    bce = keras.losses.binary_crossentropy(y_true, y_pred)\n    dice = 1.0 - dice_coef(y_true, y_pred)\n    return bce + dice\n\nmodel = build_unet()\nmodel.compile(\n    optimizer=keras.optimizers.Adam(1e-4),\n    loss=bce_dice_loss,\n    metrics=[dice_coef]\n)\n\nmodel.summary()\n\ncallbacks = [\n    keras.callbacks.ModelCheckpoint(\n        \"best_model_baseline.keras\",\n        monitor=\"val_dice_coef\",\n        mode=\"max\",\n        save_best_only=True,\n        verbose=1\n    ),\n    keras.callbacks.ReduceLROnPlateau(\n        monitor=\"val_loss\",\n        factor=0.5,\n        patience=3,\n        verbose=1\n    ),\n    keras.callbacks.EarlyStopping(\n        monitor=\"val_loss\",\n        patience=7,\n        restore_best_weights=True,\n        verbose=1\n    )\n]\n\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCHS,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:29:20.252223Z","iopub.execute_input":"2025-11-29T09:29:20.252534Z","iopub.status.idle":"2025-11-29T09:32:38.076651Z","shell.execute_reply.started":"2025-11-29T09:29:20.252507Z","shell.execute_reply":"2025-11-29T09:32:38.076008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Take a batch from validation set\nval_batch = next(iter(val_ds))\nval_imgs, val_masks = val_batch\n\n# Predict\npred_masks = model.predict(val_imgs)\n\ndef show_sample(idx=0):\n    img  = val_imgs[idx].numpy()\n    gt   = val_masks[idx].numpy().squeeze()\n    pred = pred_masks[idx].squeeze()\n    pred_bin = (pred > 0.5).astype(np.float32)\n\n    plt.figure(figsize=(12, 4))\n\n    plt.subplot(1, 3, 1)\n    plt.imshow(img)\n    plt.title(\"Image\")\n    plt.axis(\"off\")\n\n    plt.subplot(1, 3, 2)\n    plt.imshow(gt, cmap=\"gray\")\n    plt.title(\"Ground Truth Mask\")\n    plt.axis(\"off\")\n\n    plt.subplot(1, 3, 3)\n    plt.imshow(pred_bin, cmap=\"gray\")\n    plt.title(\"Predicted Mask\")\n    plt.axis(\"off\")\n\n    plt.show()\n\nfor i in range(min(3, val_imgs.shape[0])):\n    show_sample(i)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:33:46.79519Z","iopub.execute_input":"2025-11-29T09:33:46.795854Z","iopub.status.idle":"2025-11-29T09:33:49.806107Z","shell.execute_reply.started":"2025-11-29T09:33:46.795826Z","shell.execute_reply":"2025-11-29T09:33:49.805251Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def mask_to_rle(mask):\n    \"\"\"\n    mask: 2D array of 0/1\n    returns RLE string (1-indexed, column-major)\n    \"\"\"\n    pixels = mask.flatten(order=\"F\")\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    return \" \".join(str(x) for x in runs)\n\ndef postprocess_mask(pred, threshold=0.5, min_area=10):\n    mask = (pred > threshold).astype(np.uint8)\n    if mask.sum() < min_area:\n        return np.zeros_like(mask)\n    return mask\n\n# read sample submission; make case_id str\nss = pd.read_csv(SAMPLE_SUB_PATH, dtype={\"case_id\": str})\nss.head()\n\ndef find_test_image_path(case_id):\n    case_id = str(case_id)\n    matches = glob.glob(os.path.join(TEST_IMG_DIR, case_id + \".*\"))\n    if not matches:\n        raise FileNotFoundError(f\"No test image for case_id {case_id}\")\n    return matches[0]\n\ndef load_test_image(path):\n    img = cv2.imread(path, cv2.IMREAD_COLOR)\n    if img is None:\n        return np.zeros((*IMG_SIZE, 3), dtype=np.float32)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = cv2.resize(img, IMG_SIZE[::-1])\n    return img.astype(np.float32) / 255.0\n\nannotations = []\n\nfor _, row in ss.iterrows():\n    case_id = row[\"case_id\"]\n    img_path = find_test_image_path(case_id)\n\n    img = load_test_image(img_path)\n    pred = model.predict(img[None, ...], verbose=0)[0, :, :, 0]\n\n    mask = postprocess_mask(pred)\n\n    if mask.sum() == 0:\n        annotations.append(\"authentic\")\n    else:\n        annotations.append(mask_to_rle(mask))\n\nss[\"annotation\"] = annotations\nss.to_csv(\"submission.csv\", index=False)\nprint(\"Saved submission.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:34:09.159766Z","iopub.execute_input":"2025-11-29T09:34:09.160415Z","iopub.status.idle":"2025-11-29T09:34:12.38811Z","shell.execute_reply.started":"2025-11-29T09:34:09.160388Z","shell.execute_reply":"2025-11-29T09:34:12.387425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import socket\ntry:\n    socket.gethostbyname(\"google.com\")\n    print(\"Internet ON\")\nexcept:\n    print(\"Internet OFF\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-29T09:35:53.837046Z","iopub.execute_input":"2025-11-29T09:35:53.837846Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}