{"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":"none","dataSources":[{"sourceId":113558,"databundleVersionId":14174843,"isSourceIdPinned":false,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport warnings\nwarnings.filterwarnings('ignore')\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\nimport cv2\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# =============================================================================\n# CONFIG\n# =============================================================================\nclass Config:\n    BASE_PATH = '/kaggle/input/recodai-luc-scientific-image-forgery-detection'\n    TRAIN_IMAGES_FORGED = os.path.join(BASE_PATH, 'train_images/forged')\n    TRAIN_IMAGES_AUTH = os.path.join(BASE_PATH, 'train_images/authentic')\n    TRAIN_MASKS = os.path.join(BASE_PATH, 'train_masks')\n    TEST_IMAGES = os.path.join(BASE_PATH, 'test_images')\n    SAMPLE_SUB = os.path.join(BASE_PATH, 'sample_submission.csv')\n\n    # SIFT parameters\n    SIFT_FEATURES = 5500\n    SIFT_CONTRAST = 0.019\n    MATCH_RATIO = 0.79\n    MIN_MATCHES = 4\n    RANSAC_THRESH = 5.5\n    MIN_DISPLACEMENT = 23\n    MAX_IMAGE_SIZE = 1600\n    USE_CLAHE = True\n\n    # Variant tweaks\n    SIFT_CONFIDENCE_THRESHOLD = 0.31\n    SIFT_MIN_MASK_PIXELS = 85\n    SIFT_MIN_COVERAGE = 0.00035\n    SIFT_MAX_COVERAGE = 0.42\n\n    # CNN parameters\n    CNN_ENABLED = True\n    CNN_IMAGE_SIZE = 256\n    CNN_THRESHOLD = 0.5\n\n    # Ensemble\n    ENSEMBLE_MODE = 'weighted'\n    SIFT_WEIGHT = 0.75\n    CNN_WEIGHT = 0.25\n\nconfig = Config()\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nprint(f\"🎯 HYBRID ENSEMBLE - SIFT + Fast CNN\")\nprint(f\" Device: {device}\")\n\n# =============================================================================\n# Dummy CNN Model (replace with your trained model)\n# =============================================================================\nclass SimpleCNN(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.conv1 = nn.Conv2d(3, 8, 3, padding=1)\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.fc = nn.Linear(8,1)\n    def forward(self,x):\n        x = F.relu(self.conv1(x))\n        x = self.pool(x)\n        x = x.view(x.size(0), -1)\n        x = torch.sigmoid(self.fc(x))\n        return x\n\ncnn_model = SimpleCNN().to(device)\ncnn_model.eval()\n\n# =============================================================================\n# SIFT scoring\n# =============================================================================\ndef sift_score(img_path, ref_path):\n    img = cv2.imread(img_path, 0)\n    ref = cv2.imread(ref_path, 0)\n    if img is None or ref is None:\n        return 0.0\n    if config.USE_CLAHE:\n        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))\n        img = clahe.apply(img)\n        ref = clahe.apply(ref)\n    sift = cv2.SIFT_create(nfeatures=config.SIFT_FEATURES, contrastThreshold=config.SIFT_CONTRAST)\n    kp1, des1 = sift.detectAndCompute(img, None)\n    kp2, des2 = sift.detectAndCompute(ref, None)\n    if des1 is None or des2 is None:\n        return 0.0\n    bf = cv2.BFMatcher()\n    matches = bf.knnMatch(des1, des2, k=2)\n    good = []\n    for m,n in matches:\n        if m.distance < config.MATCH_RATIO * n.distance:\n            good.append(m)\n    score = len(good) / max(len(matches),1)\n    return float(score)\n\n# =============================================================================\n# CNN scoring\n# =============================================================================\ndef cnn_score(img_path):\n    img = Image.open(img_path).convert('RGB').resize((config.CNN_IMAGE_SIZE, config.CNN_IMAGE_SIZE))\n    img = np.array(img)/255.0\n    img = torch.tensor(img).permute(2,0,1).unsqueeze(0).float().to(device)\n    with torch.no_grad():\n        out = cnn_model(img).item()\n    return float(out)\n\n# =============================================================================\n# Generate Submission\n# =============================================================================\ndef generate_submission():\n    sub = pd.read_csv(config.SAMPLE_SUB)\n    preds = []\n    # Use first authentic image as reference\n    ref_img_name = os.listdir(config.TRAIN_IMAGES_AUTH)[0]\n    ref_path = os.path.join(config.TRAIN_IMAGES_AUTH, ref_img_name)\n\n    for img_name in tqdm(sub['case_id']):\n        img_name_str = str(img_name) + \".png\"  # Convert to string and add extension\n        test_path = os.path.join(config.TEST_IMAGES, img_name_str)\n        s_score = sift_score(test_path, ref_path)\n        c_score = cnn_score(test_path) if config.CNN_ENABLED else 0.0\n\n        # Ensemble\n        if config.ENSEMBLE_MODE == 'weighted':\n            final_score = config.SIFT_WEIGHT*s_score + config.CNN_WEIGHT*c_score\n        else: # voting\n            final_score = 1.0 if (s_score>0.5 or c_score>config.CNN_THRESHOLD) else 0.0\n        preds.append(final_score)\n\n    sub['annotation'] = preds\n    output_file = \"submission.csv\"\n    sub.to_csv(output_file, index=False)\n    print(f\"✅ Submission saved to {output_file}\")\n\n# =============================================================================\n# MAIN\n# =============================================================================\nif __name__ == \"__main__\":\n    generate_submission()\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-06T01:56:31.763171Z","iopub.execute_input":"2025-11-06T01:56:31.763584Z","iopub.status.idle":"2025-11-06T01:56:42.951195Z","shell.execute_reply.started":"2025-11-06T01:56:31.763542Z","shell.execute_reply":"2025-11-06T01:56:42.950069Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}