{"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":[{"sourceType":"competition","sourceId":113558,"databundleVersionId":14878066},{"sourceType":"modelInstanceVersion","sourceId":90860,"databundleVersionId":9303146,"modelInstanceId":76172},{"sourceType":"kernelVersion","sourceId":281975495}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Visualization","metadata":{}},{"cell_type":"code","source":"import os\nimport glob\n\nPATH_DATASET = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\nauthentic_images = glob.glob(os.path.join(PATH_DATASET, 'train_images', 'authentic', '*.png'))\nforged_images = glob.glob(os.path.join(PATH_DATASET, 'train_images', 'forged', '*.png'))\nforged_images += glob.glob(os.path.join(PATH_DATASET, 'supplemental_images', '*.png'))\n\nprint(f\"Found {len(authentic_images)} authentic images.\")\nprint(f\"Found {len(forged_images)} forged images.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:07:52.918153Z","iopub.execute_input":"2025-12-21T20:07:52.918657Z","iopub.status.idle":"2025-12-21T20:07:53.012263Z","shell.execute_reply.started":"2025-12-21T20:07:52.918622Z","shell.execute_reply":"2025-12-21T20:07:53.011215Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Authentic cases","metadata":{}},{"cell_type":"code","source":"import random\nimport matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\n\n# Select a random subset of authentic images\nnum_images_to_show = 12  # 3x4 grid\nrandom_authentic_images = random.sample(authentic_images, min(num_images_to_show, len(authentic_images)))\n\n# Display the images in a grid\nfig, axes = plt.subplots(3, 4, figsize=(10, 8))\naxes = axes.flatten()\n\nfor i, img_path in enumerate(random_authentic_images):\n    img = mpimg.imread(img_path)\n    axes[i].imshow(img)\n    axes[i].axis('off') # Hide axes\n    axes[i].set_title(os.path.basename(img_path), fontsize=8) # Add filename as title\n\n# Hide any unused subplots\nfor j in range(i + 1, len(axes)):\n    axes[j].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:07:53.013311Z","iopub.execute_input":"2025-12-21T20:07:53.013607Z","iopub.status.idle":"2025-12-21T20:08:00.789490Z","shell.execute_reply.started":"2025-12-21T20:07:53.013563Z","shell.execute_reply":"2025-12-21T20:08:00.788454Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Forged cases with annotations","metadata":{}},{"cell_type":"code","source":"import glob\n\n# Assuming the masks are in a 'train_masks' directory within the data_dir\nmask_dir = os.path.join(PATH_DATASET, 'train_masks')\n\n# Find all .npy files in the train_masks directory and store in a dictionary\nmask_files_dict = {}\nmask_files = glob.glob(os.path.join(mask_dir, '*.npy'))\nmask_files += glob.glob(os.path.join(PATH_DATASET, 'supplemental_masks', '*.npy'))\nfor mask_path in mask_files:\n    basename = os.path.basename(mask_path)\n    filename_without_extension, _ = os.path.splitext(basename) # Remove extension\n    mask_files_dict[filename_without_extension] = mask_path\n\nprint(f\"Found {len(mask_files_dict)} mask files and stored in a dictionary with keys as filenames without extensions.\")\nmask_dict = [f\"{k}: {v}\" for k, v in list(mask_files_dict.items())]\nprint(\"\\n\".join(mask_dict[:5]))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:17:23.388130Z","iopub.execute_input":"2025-12-21T20:17:23.388713Z","iopub.status.idle":"2025-12-21T20:17:23.431112Z","shell.execute_reply.started":"2025-12-21T20:17:23.388676Z","shell.execute_reply":"2025-12-21T20:17:23.430107Z"},"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tqdm.auto import tqdm\nimport numpy as np\n\n# Load all masks and store their shapes\nall_mask_shapes = []\nfor filename_without_extension, mask_path in tqdm(mask_files_dict.items()):\n    mask = np.load(mask_path)\n    all_mask_shapes.append(len(mask.shape))\n\nprint(f\"Loaded shapes for {len(all_mask_shapes)} masks.\")\nprint(\"Mask shapes:\", set(all_mask_shapes))","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-12-21T20:17:25.975643Z","iopub.execute_input":"2025-12-21T20:17:25.976664Z","iopub.status.idle":"2025-12-21T20:17:32.546592Z","shell.execute_reply.started":"2025-12-21T20:17:25.976625Z","shell.execute_reply":"2025-12-21T20:17:32.545469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_mask(mask_path: str):\n    mask_raw = np.load(mask_path)\n    # Sum across the first dimension and binarize: 1 if any channel has a value > 0, 0 otherwise.\n    mask = np.zeros_like(mask_raw[0, :, :], dtype=np.uint8)\n    for c in range(mask_raw.shape[0]):\n        mask[mask_raw[c, :, :] > 0] = c + 1\n    return mask","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:17:32.548165Z","iopub.execute_input":"2025-12-21T20:17:32.548422Z","iopub.status.idle":"2025-12-21T20:17:32.553976Z","shell.execute_reply.started":"2025-12-21T20:17:32.548403Z","shell.execute_reply":"2025-12-21T20:17:32.552673Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 1: Match images and masks\n# We'll match based on the base filename (without extension)\nimage_mask_pairs = []\n\nfor image_path in forged_images:\n    image_basename = os.path.basename(image_path)\n    filename_without_extension, _ = os.path.splitext(image_basename)\n    mask_path = mask_files_dict[filename_without_extension]\n    image_mask_pairs.append((image_path, mask_path))\n\nprint(f\"Found {len(image_mask_pairs)} image-mask pairs.\")","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-12-21T20:17:32.555123Z","iopub.execute_input":"2025-12-21T20:17:32.555407Z","iopub.status.idle":"2025-12-21T20:17:32.582561Z","shell.execute_reply.started":"2025-12-21T20:17:32.555377Z","shell.execute_reply":"2025-12-21T20:17:32.581680Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define a list of colors for the different mask levels (excluding background 0)\n# You can customize this list with more colors if you expect more levels\nmask_colors = ['red', 'blue', 'green', 'purple', 'orange', 'brown', 'pink', 'gray', 'olive', 'cyan']","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-12-21T20:17:35.505566Z","iopub.execute_input":"2025-12-21T20:17:35.506119Z","iopub.status.idle":"2025-12-21T20:17:35.510365Z","shell.execute_reply.started":"2025-12-21T20:17:35.506093Z","shell.execute_reply":"2025-12-21T20:17:35.509337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Step 2: Select random subset\nnum_pairs_to_show = 12 # For a 12-row grid\nrandom_pairs = random.sample(image_mask_pairs, min(num_pairs_to_show, len(image_mask_pairs)))\n\nfor i, (image_path, mask_path) in enumerate(random_pairs):\n    # Step 3: Visualize in grid (3 columns, num_pairs_to_show rows)\n    fig, axes = plt.subplots(1, 3, figsize=(12, 4)) # Adjust figsize as needed\n    # Display image in the first column\n    img = mpimg.imread(image_path)\n    axes[0].imshow(img)\n    axes[0].axis('off')\n    axes[0].set_title(os.path.basename(image_path), fontsize=8)\n\n    # Load the mask as multilabel\n    mask = load_mask(mask_path)\n    levels = np.unique(mask)[:-1] + 0.5\n\n    # Display image with mask contour in the second column\n    axes[1].imshow(img) # Display the original image\n\n    # Find and draw contours on the second column axes\n    axes[1].contour(mask, levels=levels, colors=mask_colors, linewidths=1)\n    axes[1].axis('off')\n    axes[1].set_title(\"Mask Contour\", fontsize=8)\n\n    # Display mask in the third column\n    # Assuming the mask is a grayscale or binary image, adjust colormap if necessary\n    axes[2].imshow(mask, cmap='viridis', interpolation='nearest')\n    axes[2].axis('off')\n    axes[2].set_title(os.path.basename(mask_path), fontsize=8)\n    fig.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:17:37.247698Z","iopub.execute_input":"2025-12-21T20:17:37.248024Z","iopub.status.idle":"2025-12-21T20:17:44.455616Z","shell.execute_reply.started":"2025-12-21T20:17:37.248003Z","shell.execute_reply":"2025-12-21T20:17:44.454589Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Overlap authentic, forged cases and SAM2 segmentation\n\nSee a simple demo in https://www.kaggle.com/code/jirkaborovec/forgery-using-sam2-for-candidate-generation\nand refer to this notebook for the compiled `sam2` package...","metadata":{}},{"cell_type":"code","source":"!pip install -q sam_2 --no-index -f \"/kaggle/input/forgery-using-sam2-for-candidate-generation/packages/\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:18:36.312652Z","iopub.execute_input":"2025-12-21T20:18:36.313558Z","iopub.status.idle":"2025-12-21T20:22:13.905816Z","shell.execute_reply.started":"2025-12-21T20:18:36.313526Z","shell.execute_reply":"2025-12-21T20:22:13.904102Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get just the filenames without the path\nauthentic_filenames = [os.path.basename(img_path) for img_path in authentic_images]\nforged_filenames = [os.path.basename(img_path) for img_path in forged_images]\n\n# Find the intersection of the two sets of filenames\noverlapping_filenames = list(set(authentic_filenames).intersection(forged_filenames))\n\nprint(f\"Found {len(overlapping_filenames)} overlapping filenames in authentic and forged folders.\")","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-12-21T20:22:13.909406Z","iopub.execute_input":"2025-12-21T20:22:13.909805Z","iopub.status.idle":"2025-12-21T20:22:13.926584Z","shell.execute_reply.started":"2025-12-21T20:22:13.909779Z","shell.execute_reply":"2025-12-21T20:22:13.925400Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create dictionaries mapping filename without extension to full path for quicker lookup\nauthentic_image_dict = {os.path.splitext(os.path.basename(img_path))[0]: img_path for img_path in authentic_images}\nforged_image_dict = {os.path.splitext(os.path.basename(img_path))[0]: img_path for img_path in forged_images}\n\n# Find filenames that exist in both authentic and forged sets (using keys without extensions)\noverlapping_filenames_without_extension = list(set(authentic_image_dict.keys()).intersection(forged_image_dict.keys()))\n\n# Create pairs of (authentic_path, forged_path, mask_path) for overlapping filenames\nmatching_pairs_with_mask = []\nfor filename_without_extension in overlapping_filenames_without_extension:\n    authentic_path = authentic_image_dict[filename_without_extension]\n    forged_path = forged_image_dict[filename_without_extension]\n    # Check if a mask exists for this forged image (mask_files_dict already uses keys without extension)\n    if filename_without_extension in mask_files_dict:\n        mask_path = mask_files_dict[filename_without_extension]\n        matching_pairs_with_mask.append((authentic_path, forged_path, mask_path))\n    else:\n        print(f\"Warning: No mask found for forged image with filename (without extension): {filename_without_extension}\")\n\nprint(f\"Found {len(matching_pairs_with_mask)} matching image-mask pairs with the same filename (without extension).\")","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-12-21T20:22:13.927636Z","iopub.execute_input":"2025-12-21T20:22:13.927930Z","iopub.status.idle":"2025-12-21T20:22:13.960905Z","shell.execute_reply.started":"2025-12-21T20:22:13.927904Z","shell.execute_reply":"2025-12-21T20:22:13.959855Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom sam2.build_sam import build_sam2\nfrom sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Specify the path to the pre-trained model weights file.\n# This corresponds to the \"brain\" of the AI.\ncheckpoint_path = \"/kaggle/input/segment-anything-2/pytorch/sam2-hiera-base-plus/1/sam2_hiera_base_plus.pt\"\n\n# Specify the configuration file, which acts as the model's blueprint.\nmodel_config = \"sam2_hiera_b+.yaml\"\n\n# Build the SAM2 model using the blueprint and the weight file.\n# apply_postprocessing=False is set to obtain the raw output from the model.\nsam2_model = build_sam2(model_config, checkpoint_path, device=device, apply_postprocessing=False)\n\n# Create a \"Mask Generator\" to automatically detect all objects within the image.\nmask_generator = SAM2AutomaticMaskGenerator(sam2_model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:22:13.962251Z","iopub.execute_input":"2025-12-21T20:22:13.962918Z","iopub.status.idle":"2025-12-21T20:22:21.690879Z","shell.execute_reply.started":"2025-12-21T20:22:13.962800Z","shell.execute_reply":"2025-12-21T20:22:21.689937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def display_image_with_annotations(image, annotations, ax):\n    \"\"\"Overlays masks on an image and returns the Matplotlib Figure object.\"\"\"\n    # 2. Display the base image on the axes\n    ax.imshow(image)\n    ax.axis('off') # Hide the axis ruler/numbers\n    # 3. If no annotations, return the figure with just the base image\n    if not annotations:\n        return fig\n    # 4. Sort masks: Largest first\n    annotations.sort(key=lambda x: x['area'], reverse=True)\n    # 5. Create the RGBA overlay layer\n    h, w = image.shape[:2]\n    overlay_rgba = np.zeros((h, w, 4), dtype=np.float32)\n    # 6. Draw masks onto the overlay layer\n    for ann in annotations:\n        mask = ann['segmentation']\n        rgb = np.random.random(3) # Random RGB color\n        overlay_rgba[mask, :3] = rgb # Color\n        overlay_rgba[mask, 3] = 0.5  # Alpha (Transparency)\n    # 7. Add the overlay to the axes\n    ax.imshow(overlay_rgba)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-21T20:22:21.693553Z","iopub.execute_input":"2025-12-21T20:22:21.693953Z","iopub.status.idle":"2025-12-21T20:22:21.700775Z","shell.execute_reply.started":"2025-12-21T20:22:21.693929Z","shell.execute_reply":"2025-12-21T20:22:21.699710Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Determine the number of pairs to show (up to 12 rows)\nnum_pairs_to_show = min(16, len(matching_pairs_with_mask))\n\n# Select a random subset of matching pairs\nrandom_matching_pairs_with_mask = random.sample(matching_pairs_with_mask, num_pairs_to_show)\n\nfor i in range(num_pairs_to_show):\n    # Create the grid (num_pairs_to_show rows, 3 columns)\n    fig, axes = plt.subplots(1, 3, figsize=(18, 6)) # Adjust figsize as needed\n    auth_img_path, forged_img_path, mask_path = random_matching_pairs_with_mask[i]\n    auth_img = mpimg.imread(auth_img_path)\n    forged_img = mpimg.imread(forged_img_path)\n    # Load the mask as multilabel\n    mask = load_mask(mask_path)\n    levels = np.unique(mask)[:-1] + 0.5\n    # normalize image\n    if forged_img.ndim == 2: # Grayscale image\n        image = np.stack([forged_img, forged_img, forged_img], axis=-1)\n    else:\n        image = forged_img[..., :3] # Ensure RGB or RGBA becomes RGB\n    # generate intance segmentaions\n    sam_masks = mask_generator.generate(image)\n\n    # Display authentic image in the first column\n    axes[0].imshow(auth_img)\n    # Find and draw contours on the second column axes using the mask\n    axes[0].contour(mask, levels=levels, colors=mask_colors, linewidths=1)\n    axes[0].axis('off')\n    axes[0].set_title(f\"Authentic + Mask: {os.path.basename(auth_img_path)}\", fontsize=8)\n\n    # Display forged image with mask contour in the second column\n    axes[1].imshow(forged_img) # Display the forged image\n    # Find and draw contours on the second column axes using the mask\n    axes[1].contour(mask, levels=levels, colors=mask_colors, linewidths=1)\n    axes[1].axis('off')\n    axes[1].set_title(f\"Forged + Mask: {os.path.basename(forged_img_path)}\", fontsize=8)\n\n    display_image_with_annotations(image, sam_masks, ax=axes[2])\n    fig.tight_layout()\nplt.show()","metadata":{"trusted":true,"_kg_hide-input":false,"execution":{"iopub.status.busy":"2025-12-21T20:22:21.701759Z","iopub.execute_input":"2025-12-21T20:22:21.702045Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Analyses","metadata":{}},{"cell_type":"code","source":"all_mask_instances = []\nfor filename_without_extension, mask_path in tqdm(mask_files_dict.items()):\n    mask = np.load(mask_path)\n    all_mask_instances.append(mask.shape[0])\n\nprint(f\"Loaded shapes for {len(all_mask_instances)} masks.\")\nprint(\"Mask shapes:\", set(all_mask_instances))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Discover how many instances are per image","metadata":{}},{"cell_type":"code","source":"import collections\nimport seaborn as sns\n\n# Count the occurrences of each instance count\ninstance_counts = collections.Counter(all_mask_instances)\nsorted_instance_counts = dict(sorted(instance_counts.items()))\n\n# Create a bar plot of the instance counts\nplt.figure(figsize=(8, 3))\nsns.barplot(x=list(sorted_instance_counts.keys()), y=list(sorted_instance_counts.values()))\nplt.title(\"Mask Instance Counts\")\nplt.xlabel(\"Number of Instances in Mask\")\nplt.ylabel(\"Occurances\")\nplt.grid(axis='y', alpha=0.75)\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Explore the ratios of object to image size","metadata":{}},{"cell_type":"code","source":"# Initialize a list to store all area ratios\nall_area_ratios = []\n\n# Iterate through mask files\nfor filename_without_extension, mask_path in tqdm(mask_files_dict.items()):\n    # Load the raw mask data (not using the load_mask function as we need individual layers)\n    mask_raw = np.load(mask_path)\n    # Get image dimensions from the mask shape (assuming mask and image have same dimensions)\n    num_instances, height, width = mask_raw.shape\n    total_image_area = height * width\n    # List to store ratios for the current mask\n    mask_area_ratios = []\n\n    # Iterate through mask instances (layers)\n    for instance_layer in mask_raw:\n        # Calculate segmented area for the instance\n        segmented_area = np.sum(instance_layer > 0)\n        # Calculate area ratio\n        area_ratio = segmented_area / total_image_area\n        # Store the ratio\n        mask_area_ratios.append(area_ratio)\n\n    # Extend the main list with ratios from the current mask\n    all_area_ratios.extend(mask_area_ratios)\n\nprint(f\"Calculated area ratios for {len(all_area_ratios)} instances across all masks.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Analyze and visualize the distribution of area ratios\nplt.figure(figsize=(10, 4))\nsns.histplot(all_area_ratios, bins=50, kde=True) # Using 50 bins to show the distribution shape\nplt.title(\"Distribution of Segmented Area Ratios\")\nplt.xlabel(\"Area Ratio (Segmented Area / Total Image Area)\")\nplt.ylabel(\"Frequency\")\nplt.grid(axis='y', alpha=0.75)\nplt.show()\n\n# Print some basic statistics about the area ratios\nprint(\"\\nBasic statistics for area ratios:\")\nprint(f\"Mean: {np.mean(all_area_ratios):.4f}\")\nprint(f\"Median: {np.median(all_area_ratios):.4f}\")\nprint(f\"Standard Deviation: {np.std(all_area_ratios):.4f}\")\nprint(f\"Min: {np.min(all_area_ratios):.4f}\")\nprint(f\"Max: {np.max(all_area_ratios):.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}