{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"nvidiaTeslaT4","dataSources":[{"sourceId":113558,"databundleVersionId":14878066,"sourceType":"competition"},{"sourceId":287301665,"sourceType":"kernelVersion"}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":243.846583,"end_time":"2025-11-12T09:06:39.114105","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-11-12T09:02:35.267522","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"049cea5726bd40f4ae599db9e29ca42e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"3346fae9af6b46aa9c505804eb0cc572":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"3fe604dc208b4bec9252f8f306a62bf7":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"4549384b78d141ce85612799a0e117a2":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_cdf9e72d8e82449f93362eb04657bc70","IPY_MODEL_6759369cbc9a4aaeb709a6bf3fa696c5","IPY_MODEL_7817110464ae403c9ad9a17cdaa7532c"],"layout":"IPY_MODEL_dd987052f62144bcab17bcdb33fa35c7","tabbable":null,"tooltip":null}},"46a9cddd18e142f2aa275258c33ea32d":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"55049d13ff3e4ef19ff3b63f1e07760a":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_3346fae9af6b46aa9c505804eb0cc572","placeholder":"​","style":"IPY_MODEL_f3331a7415a841fca08b397d0ac3f27e","tabbable":null,"tooltip":null,"value":" 5/5 [02:49&lt;00:00, 33.74s/it]"}},"6759369cbc9a4aaeb709a6bf3fa696c5":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_84cbcf7c82174d4a8c9cf424d55cc9f4","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_69968ec8724e4775bb1ea05fafacb4ae","tabbable":null,"tooltip":null,"value":1}},"69968ec8724e4775bb1ea05fafacb4ae":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"6cad44b595274fa29ff4efac75e240e4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"6d884bcb1fb647f2ba3dc10023a02cc0":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"6ec860e1528543fd9eb9c3fcdde58d9b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"7817110464ae403c9ad9a17cdaa7532c":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_e7e9a1a107d14022b741546c33b6d4d9","placeholder":"​","style":"IPY_MODEL_6d884bcb1fb647f2ba3dc10023a02cc0","tabbable":null,"tooltip":null,"value":" 1/1 [00:01&lt;00:00,  1.46s/it]"}},"7eabc6e9647e4891b8dffa51cb123b3b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_898f62ec11894b1da97720c3129cab2e","IPY_MODEL_b2cb97fd4894428381c363b8869e96c4","IPY_MODEL_55049d13ff3e4ef19ff3b63f1e07760a"],"layout":"IPY_MODEL_dc6db15174954f6095dc43b4a879fe91","tabbable":null,"tooltip":null}},"84cbcf7c82174d4a8c9cf424d55cc9f4":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"898f62ec11894b1da97720c3129cab2e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_3fe604dc208b4bec9252f8f306a62bf7","placeholder":"​","style":"IPY_MODEL_6ec860e1528543fd9eb9c3fcdde58d9b","tabbable":null,"tooltip":null,"value":"Loading models: 100%"}},"b2cb97fd4894428381c363b8869e96c4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_46a9cddd18e142f2aa275258c33ea32d","max":5,"min":0,"orientation":"horizontal","style":"IPY_MODEL_cdd3f6c6c8114305a6c93ac3ad6983c0","tabbable":null,"tooltip":null,"value":5}},"cdd3f6c6c8114305a6c93ac3ad6983c0":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"cdf9e72d8e82449f93362eb04657bc70":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_049cea5726bd40f4ae599db9e29ca42e","placeholder":"​","style":"IPY_MODEL_6cad44b595274fa29ff4efac75e240e4","tabbable":null,"tooltip":null,"value":"Running inference: 100%"}},"dc6db15174954f6095dc43b4a879fe91":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"dd987052f62144bcab17bcdb33fa35c7":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"e7e9a1a107d14022b741546c33b6d4d9":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"f3331a7415a841fca08b397d0ac3f27e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Imports and configs","metadata":{"papermill":{"duration":0.005756,"end_time":"2025-11-12T09:02:38.685623","exception":false,"start_time":"2025-11-12T09:02:38.679867","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import json\n\nimport numba\nimport numpy as np\nfrom numba import types\nimport numpy.typing as npt\nimport pandas as pd\nimport scipy.optimize\n\n\nclass ParticipantVisibleError(Exception):\n    pass\n\n\n@numba.jit(nopython=True)\ndef _rle_encode_jit(x: npt.NDArray, fg_val: int = 1) -> list[int]:\n    \"\"\"Numba-jitted RLE encoder.\"\"\"\n    dots = np.where(x.T.flatten() == fg_val)[0]\n    run_lengths = []\n    prev = -2\n    for b in dots:\n        if b > prev + 1:\n            run_lengths.extend((b + 1, 0))\n        run_lengths[-1] += 1\n        prev = b\n    return run_lengths\n\n\ndef rle_encode(mask):\n    mask = mask.astype(bool)\n    flat = mask.T.flatten()\n    dots = np.where(flat)[0]\n    \n    if len(dots) == 0:\n        return json.dumps([])\n    \n    run_lengths = []\n    prev = -2\n    for b in dots:\n        if b > prev + 1:\n            run_lengths.extend([b + 1, 0])\n        run_lengths[-1] += 1\n        prev = b\n    \n    run_lengths = [int(x) for x in run_lengths]\n    return json.dumps(run_lengths)\n\n\n@numba.njit\ndef _rle_decode_jit(mask_rle: npt.NDArray, height: int, width: int) -> npt.NDArray:\n    \"\"\"\n    s: numpy array of run-length encoding pairs (start, length)\n    shape: (height, width) of array to return\n    Returns numpy array, 1 - mask, 0 - background\n    \"\"\"\n    if len(mask_rle) % 2 != 0:\n        # Numba requires raising a standard exception.\n        raise ValueError('One or more rows has an odd number of values.')\n\n    starts, lengths = mask_rle[0::2], mask_rle[1::2]\n    starts -= 1\n    ends = starts + lengths\n    for i in range(len(starts) - 1):\n        if ends[i] > starts[i + 1]:\n            raise ValueError('Pixels must not be overlapping.')\n    img = np.zeros(height * width, dtype=np.bool_)\n    for lo, hi in zip(starts, ends):\n        img[lo:hi] = 1\n    return img\n\n\ndef rle_decode(mask_rle: str, shape: tuple[int, int]) -> npt.NDArray:\n    \"\"\"\n    mask_rle: run-length as string formatted (start length)\n              empty predictions need to be encoded with '-'\n    shape: (height, width) of array to return\n    Returns numpy array, 1 - mask, 0 - background\n    \"\"\"\n\n    mask_rle = json.loads(mask_rle)\n    mask_rle = np.asarray(mask_rle, dtype=np.int32)\n    starts = mask_rle[0::2]\n    if sorted(starts) != list(starts):\n        raise ParticipantVisibleError('Submitted values must be in ascending order.')\n    try:\n        return _rle_decode_jit(mask_rle, shape[0], shape[1]).reshape(shape, order='F')\n    except ValueError as e:\n        raise ParticipantVisibleError(str(e)) from e\n\n\ndef calculate_f1_score(pred_mask: npt.NDArray, gt_mask: npt.NDArray):\n    pred_flat = pred_mask.flatten()\n    gt_flat = gt_mask.flatten()\n\n    tp = np.sum((pred_flat == 1) & (gt_flat == 1))\n    fp = np.sum((pred_flat == 1) & (gt_flat == 0))\n    fn = np.sum((pred_flat == 0) & (gt_flat == 1))\n\n    precision = tp / (tp + fp) if (tp + fp) > 0 else 0\n    recall = tp / (tp + fn) if (tp + fn) > 0 else 0\n\n    if (precision + recall) > 0:\n        return 2 * (precision * recall) / (precision + recall)\n    else:\n        return 0\n\n\ndef calculate_f1_matrix(pred_masks: list[npt.NDArray], gt_masks: list[npt.NDArray]):\n    \"\"\"\n    Parameters:\n    pred_masks (np.ndarray):\n            First dimension is the number of predicted instances.\n            Each instance is a binary mask of shape (height, width).\n    gt_masks (np.ndarray):\n            First dimension is the number of ground truth instances.\n            Each instance is a binary mask of shape (height, width).\n    \"\"\"\n\n    num_instances_pred = len(pred_masks)\n    num_instances_gt = len(gt_masks)\n    f1_matrix = np.zeros((num_instances_pred, num_instances_gt))\n\n    # Calculate F1 scores for each pair of predicted and ground truth masks\n    for i in range(num_instances_pred):\n        for j in range(num_instances_gt):\n            pred_flat = pred_masks[i].flatten()\n            gt_flat = gt_masks[j].flatten()\n            f1_matrix[i, j] = calculate_f1_score(pred_mask=pred_flat, gt_mask=gt_flat)\n\n    if f1_matrix.shape[0] < len(gt_masks):\n        # Add a row of zeros to the matrix if the number of predicted instances is less than ground truth instances\n        f1_matrix = np.vstack((f1_matrix, np.zeros((len(gt_masks) - len(f1_matrix), num_instances_gt))))\n\n    return f1_matrix\n\n\ndef oF1_score(pred_masks: list[npt.NDArray], gt_masks: list[npt.NDArray]):\n    \"\"\"\n    Calculate the optimal F1 score for a set of predicted masks against\n    ground truth masks which considers the optimal F1 score matching.\n    This function uses the Hungarian algorithm to find the optimal assignment\n    of predicted masks to ground truth masks based on the F1 score matrix.\n    If the number of predicted masks is less than the number of ground truth masks,\n    it will add a row of zeros to the F1 score matrix to ensure that the dimensions match.\n\n    Parameters:\n    pred_masks (list of np.ndarray): List of predicted binary masks.\n    gt_masks (np.ndarray): Array of ground truth binary masks.\n    Returns:\n    float: Optimal F1 score.\n    \"\"\"\n    f1_matrix = calculate_f1_matrix(pred_masks, gt_masks)\n\n    # Find the best matching between predicted and ground truth masks\n    row_ind, col_ind = scipy.optimize.linear_sum_assignment(-f1_matrix)\n    # The linear_sum_assignment discards excess predictions so we need a separate penalty.\n    excess_predictions_penalty = len(gt_masks) / max(len(pred_masks), len(gt_masks))\n    return np.mean(f1_matrix[row_ind, col_ind]) * excess_predictions_penalty\n\n\ndef evaluate_single_image(label_rles: str, prediction_rles: str, shape_str: str) -> float:\n    shape = json.loads(shape_str)\n    label_rles = [rle_decode(x, shape=shape) for x in label_rles.split(';')]\n    prediction_rles = [rle_decode(x, shape=shape) for x in prediction_rles.split(';')]\n    return oF1_score(prediction_rles, label_rles)\n\n\ndef score(solution: pd.DataFrame, submission: pd.DataFrame, row_id_column_name: str) -> float:\n    \"\"\"\n    Args:\n        solution (pd.DataFrame): The ground truth DataFrame.\n        submission (pd.DataFrame): The submission DataFrame.\n        row_id_column_name (str): The name of the column containing row IDs.\n    Returns:\n        float\n\n    Examples\n    --------\n    >>> solution = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['authentic', 'authentic', 'authentic'], 'shape': ['authentic', 'authentic', 'authentic']})\n    >>> submission = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['authentic', 'authentic', 'authentic']})\n    >>> score(solution.copy(), submission.copy(), row_id_column_name='row_id')\n    1.0\n\n    >>> solution = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['authentic', 'authentic', 'authentic'], 'shape': ['authentic', 'authentic', 'authentic']})\n    >>> submission = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['[101, 102]', '[101, 102]', '[101, 102]']})\n    >>> score(solution.copy(), submission.copy(), row_id_column_name='row_id')\n    0.0\n\n    >>> solution = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['[101, 102]', '[101, 102]', '[101, 102]'], 'shape': ['[720, 960]', '[720, 960]', '[720, 960]']})\n    >>> submission = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['[101, 102]', '[101, 102]', '[101, 102]']})\n    >>> score(solution.copy(), submission.copy(), row_id_column_name='row_id')\n    1.0\n\n    >>> solution = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['[101, 103]', '[101, 102]', '[101, 102]'], 'shape': ['[720, 960]', '[720, 960]', '[720, 960]']})\n    >>> submission = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['[101, 102]', '[101, 102]', '[101, 102]']})\n    >>> score(solution.copy(), submission.copy(), row_id_column_name='row_id')\n    0.9983739837398374\n\n    >>> solution = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['[101, 102];[300, 100]', '[101, 102]', '[101, 102]'], 'shape': ['[720, 960]', '[720, 960]', '[720, 960]']})\n    >>> submission = pd.DataFrame({'row_id': [0, 1, 2], 'annotation': ['[101, 102]', '[101, 102]', '[101, 102]']})\n    >>> score(solution.copy(), submission.copy(), row_id_column_name='row_id')\n    0.8333333333333334\n    \"\"\"\n    df = solution\n    df = df.rename(columns={'annotation': 'label'})\n\n    df['prediction'] = submission['annotation']\n    # Check for correct 'authentic' label\n    authentic_indices = (df['label'] == 'authentic') | (df['prediction'] == 'authentic')\n    df['image_score'] = ((df['label'] == df['prediction']) & authentic_indices).astype(float)\n\n    df.loc[~authentic_indices, 'image_score'] = df.loc[~authentic_indices].apply(\n        lambda row: evaluate_single_image(row['label'], row['prediction'], row['shape']), axis=1\n    )\n    return float(np.mean(df['image_score']))","metadata":{"_kg_hide-input":true,"papermill":{"duration":2.99447,"end_time":"2025-11-12T09:02:41.685069","exception":false,"start_time":"2025-11-12T09:02:38.690599","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.notebook import tqdm\nimport segmentation_models_pytorch as smp\nimport matplotlib.pyplot as plt\nimport albumentations as A\nimport pandas as pd\nimport numpy as np\nimport warnings\nimport random\nimport shutil\nimport joblib\nimport torch\nimport glob\nimport cv2\nimport os\n\nwarnings.filterwarnings('ignore')","metadata":{"_kg_hide-output":true,"papermill":{"duration":39.755778,"end_time":"2025-11-12T09:03:21.446223","exception":false,"start_time":"2025-11-12T09:02:41.690445","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    dataset_path = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\n    model_path = \"/kaggle/input/scientific-image-forgery-detection-u-net-1-2/Unet/efficientnet-b5/imagenet\"\n    \n    model_name = 'Unet'\n    encoder_name = 'efficientnet-b5'\n    encoder_weights = 'imagenet'\n    \n    n_folds = 5\n    seed = 42\n    \n    image_size = 512\n    use_tta = False","metadata":{"papermill":{"duration":0.010149,"end_time":"2025-11-12T09:03:21.461611","exception":false,"start_time":"2025-11-12T09:03:21.451462","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"os.environ['TF_CUDNN_DETERMINISTIC'] = '1'\nos.environ['TF_DETERMINISTIC_OPS'] = '1'\nos.environ['PYTHONHASHSEED'] = str(CFG.seed)\ntorch.manual_seed(CFG.seed)\ntorch.cuda.manual_seed(CFG.seed)\ntorch.cuda.manual_seed_all(CFG.seed)  \ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark = False\nnp.random.seed(CFG.seed)\nrandom.seed(CFG.seed)","metadata":{"papermill":{"duration":0.012995,"end_time":"2025-11-12T09:03:21.479831","exception":false,"start_time":"2025-11-12T09:03:21.466836","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data loading and preprocessing","metadata":{"papermill":{"duration":0.005012,"end_time":"2025-11-12T09:03:43.967341","exception":false,"start_time":"2025-11-12T09:03:43.962329","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def get_data(dataset_path, is_train=True):\n    if is_train:\n        train_image_paths = glob.glob(f\"{dataset_path}/train_images/**/*.png\")\n        supplemental_image_paths = glob.glob(f\"{dataset_path}/supplemental_images/*.png\")\n        \n        dataset = []\n        for image_path in train_image_paths:\n            image_id = image_path.split(\"/\")[-1].split(\".\")[0]\n            label = \"authentic\" if \"authentic\" in image_path else \"forged\"\n            \n            unique_case_id = f\"train_{image_id}_{label}\"\n                \n            dataset.append({\n                \"case_id\": image_id,\n                \"label\": label,\n                \"source\": \"train\",\n                \"unique_case_id\": unique_case_id,\n                \"image_path\": image_path,\n                \"mask_path\": None if label == \"authentic\" else f\"{dataset_path}/train_masks/{image_id}.npy\"\n            })\n            \n        for image_path in supplemental_image_paths:\n            image_id = image_path.split(\"/\")[-1].split(\".\")[0]\n            label = \"forged\"\n            \n            unique_case_id = f\"supplemental_{image_id}_{label}\"\n                \n            dataset.append({\n                \"case_id\": image_id,\n                \"label\": label,\n                \"source\": \"supplemental\",\n                \"unique_case_id\": unique_case_id,\n                \"image_path\": image_path,\n                \"mask_path\": f\"{dataset_path}/supplemental_masks/{image_id}.npy\"\n            })\n            \n        return pd.DataFrame(dataset)\n    \n    else:\n        image_paths = glob.glob(f\"{dataset_path}/test_images/*.png\")\n        \n        dataset = []\n        for image_path in image_paths:\n            image_id = image_path.split(\"/\")[-1].split(\".\")[0]\n                \n            dataset.append({\n                \"case_id\": image_id,\n                \"label\": None,\n                \"source\": \"test\",\n                \"unique_case_id\": f\"test_{image_id}_unknown\",\n                \"image_path\": image_path,\n                \"mask_path\": None\n            })\n            \n        return pd.DataFrame(dataset)","metadata":{"papermill":{"duration":0.011368,"end_time":"2025-11-12T09:03:43.983542","exception":false,"start_time":"2025-11-12T09:03:43.972174","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Custom dataset for training and inference","metadata":{"papermill":{"duration":0.004784,"end_time":"2025-11-12T09:03:43.993864","exception":false,"start_time":"2025-11-12T09:03:43.989080","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class CustomDataset(Dataset):\n    def __init__(self, dataset, image_size, transform, mode='train'):\n        self.dataset = dataset\n        self.image_size = image_size\n        self.transform = transform\n        self.mode = mode\n        \n    def __len__(self):\n        return len(self.dataset)\n    \n    def __getitem__(self, idx):\n        row = self.dataset.iloc[idx]\n        \n        if self.mode == 'train':\n            image = cv2.imread(row['image_path'])\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            \n            if row['label'] == 'forged':\n                mask = np.load(row[\"mask_path\"])\n                if len(mask.shape) == 3:\n                    mask = np.max(mask, axis=0)\n                else:\n                    mask = np.squeeze(mask)\n                mask = (mask > 0).astype(np.float32)\n            else:\n                mask = np.zeros((image.shape[0], image.shape[1]), dtype=np.float32)\n\n            image = cv2.resize(image, (self.image_size, self.image_size))\n            mask = cv2.resize(mask, (self.image_size, self.image_size))\n            \n            augmented = self.transform(image=image, mask=mask)\n            image = augmented['image']\n            mask = augmented['mask']\n            \n            return image, mask.unsqueeze(0)\n        \n        else:\n            image = cv2.imread(row['image_path'])\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            \n            original_size = image.shape[:2]\n            image = cv2.resize(image, (self.image_size, self.image_size))\n            \n            augmented = self.transform(image=image)\n            image = augmented['image']\n\n            return row['case_id'], row['unique_case_id'], image, original_size","metadata":{"papermill":{"duration":0.013499,"end_time":"2025-11-12T09:03:44.012253","exception":false,"start_time":"2025-11-12T09:03:43.998754","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Augmentations","metadata":{"papermill":{"duration":0.004637,"end_time":"2025-11-12T09:03:44.021846","exception":false,"start_time":"2025-11-12T09:03:44.017209","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def get_augmentations(is_train):\n    train_augmentations = A.Compose([\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),\n        A.OneOf([\n            A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=1),\n            A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=20, p=1),\n            A.RandomGamma(gamma_limit=(80, 120), p=1),\n        ], p=0.5),\n        A.OneOf([\n            A.GaussNoise(var_limit=(10.0, 50.0), p=1),\n            A.GaussianBlur(blur_limit=(3, 5), p=1)\n        ], p=0.3),\n        A.Normalize(),\n        ToTensorV2()\n    ])\n\n    val_augmentations = A.Compose([\n        A.Normalize(),\n        ToTensorV2()\n    ])\n    \n    if is_train:\n        return train_augmentations\n    else:\n        return val_augmentations","metadata":{"papermill":{"duration":0.010094,"end_time":"2025-11-12T09:03:44.036691","exception":false,"start_time":"2025-11-12T09:03:44.026597","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Inference","metadata":{"papermill":{"duration":0.004827,"end_time":"2025-11-12T09:03:44.046659","exception":false,"start_time":"2025-11-12T09:03:44.041832","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class Trainer:\n    def __init__(self, dataset_path):\n        self.dataset_path = dataset_path\n        self.device = 'cuda:0' if torch.cuda.is_available() else 'cpu'\n\n    def postprocess(self, prob_map, low_thresh, high_thresh, min_size, kernel_size):\n        strong_mask = (prob_map > high_thresh).astype(np.uint8)\n        weak_mask = (prob_map > low_thresh).astype(np.uint8)\n        \n        num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(weak_mask, connectivity=8)\n        \n        final_mask = np.zeros_like(weak_mask)\n        for i in range(1, num_labels):\n            component_mask = (labels == i).astype(np.uint8)\n            if np.bitwise_and(component_mask, strong_mask).any():\n                final_mask[labels == i] = 1\n                \n        contours, _ = cv2.findContours(final_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n        cv2.drawContours(final_mask, contours, -1, 1, thickness=cv2.FILLED)\n        \n        kernel = np.ones((kernel_size, kernel_size), np.uint8)\n        final_mask = cv2.morphologyEx(final_mask, cv2.MORPH_OPEN, kernel)\n        final_mask = cv2.morphologyEx(final_mask, cv2.MORPH_CLOSE, kernel)\n        \n        num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(final_mask, connectivity=8)\n        output_mask = np.zeros_like(final_mask)\n        \n        valid_indices = []\n        for i in range(1, num_labels):\n            if stats[i, cv2.CC_STAT_AREA] >= min_size:\n                valid_indices.append(i)\n        \n        if len(valid_indices) != 2:\n            valid_indices = []\n            \n        for i in valid_indices:\n            output_mask[labels == i] = 1\n                \n        return output_mask\n\n    def predict(self, model_paths, dataset, image_size=512, mode='test', use_tta=False):\n        def predict_with_tta(model, images):\n            model.eval()\n            predictions = []\n            \n            with torch.no_grad():\n                pred = torch.sigmoid(model(images))\n                predictions.append(pred)\n                \n                pred = torch.sigmoid(model(torch.flip(images, dims=[3])))\n                predictions.append(torch.flip(pred, dims=[3]))\n                \n                pred = torch.sigmoid(model(torch.flip(images, dims=[2])))\n                predictions.append(torch.flip(pred, dims=[2]))\n            \n            return torch.stack(predictions).mean(0)\n        \n        models = []\n        for model_path in tqdm(model_paths, desc='Loading models'):\n            model = smp.from_pretrained(model_path, encoder_weights=None).eval().to(self.device)\n            models.append(model)\n        \n        test_dataset = CustomDataset(dataset, image_size=image_size, transform=get_augmentations(is_train=False), mode=mode)\n        test_loader = DataLoader(\n            test_dataset, \n            batch_size=16,\n            shuffle=False, \n            num_workers=2,\n            pin_memory=True\n        )\n        \n        predictions = []\n        for batch_ids, _, batch_images, batch_original_sizes in tqdm(test_loader, desc='Running inference'):\n            batch_images = batch_images.to(self.device)\n            batch_size = batch_images.size(0)\n            \n            batch_fold_preds = []\n            for model in models:\n                if use_tta:\n                    batch_pred = predict_with_tta(model, batch_images)\n                else:\n                    with torch.no_grad():\n                        batch_pred = torch.sigmoid(model(batch_images))\n                \n                batch_fold_preds.append(batch_pred.cpu().numpy())\n            \n            batch_final_preds = np.mean(batch_fold_preds, axis=0).squeeze()\n\n            for i in range(batch_size):\n                image_id = batch_ids[i]\n                original_h = batch_original_sizes[0][i].item()\n                original_w = batch_original_sizes[1][i].item()\n                \n                final_pred = batch_final_preds[i]\n                mask = cv2.resize(final_pred, (original_w, original_h), interpolation=cv2.INTER_LINEAR)\n\n                mask = self.postprocess(\n                    mask, \n                    min_size=14, \n                    kernel_size=7, \n                    low_thresh=0.1385934029488286, \n                    high_thresh=0.8221253120841039\n                )\n                \n                if mask.sum() < 14:\n                    annotation = 'authentic'\n                else:\n                    annotation = rle_encode(mask)\n                \n                predictions.append({\n                    'case_id': image_id,\n                    'annotation': annotation\n                })\n        \n        return pd.DataFrame(predictions)","metadata":{"papermill":{"duration":0.018551,"end_time":"2025-11-12T09:03:44.070179","exception":false,"start_time":"2025-11-12T09:03:44.051628","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"trainer = Trainer(CFG.dataset_path)\n\nsubmission = trainer.predict(\n    model_paths=[f\"{CFG.model_path}/fold_{fold}\" for fold in range(CFG.n_folds)],\n    dataset=get_data(dataset_path=CFG.dataset_path, is_train=False),\n    image_size=CFG.image_size,\n    mode='test',\n    use_tta=CFG.use_tta\n)\n\nsubmission.to_csv(\"submission.csv\", index=False)","metadata":{"_kg_hide-output":true,"papermill":{"duration":170.849333,"end_time":"2025-11-12T09:06:34.924670","exception":false,"start_time":"2025-11-12T09:03:44.075337","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission","metadata":{"papermill":{"duration":0.023605,"end_time":"2025-11-12T09:06:34.954465","exception":false,"start_time":"2025-11-12T09:06:34.930860","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visualizing the results","metadata":{"papermill":{"duration":0.005175,"end_time":"2025-11-12T09:06:34.965117","exception":false,"start_time":"2025-11-12T09:06:34.959942","status":"completed"},"tags":[]}},{"cell_type":"code","source":"histories = joblib.load(f\"{CFG.model_path}/histories.pkl\")\nfold_scores = joblib.load(f\"{CFG.model_path}/fold_scores.pkl\")\n\nfor fold_idx, score in enumerate(fold_scores):\n    print(f\"Fold {fold_idx+1} competition score: {score:.4f}\")\n    \nprint(f\"\\nMean competition score: {np.mean(fold_scores):.4f}\")","metadata":{"papermill":{"duration":0.023669,"end_time":"2025-11-12T09:06:34.994137","exception":false,"start_time":"2025-11-12T09:06:34.970468","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(2, 2, figsize=(15, 10))\naxes = axes.flatten()\n\nfor fold_idx in range(5):\n    history = histories[fold_idx]\n    \n    axes[0].plot(history['epoch'], history['train_loss'], label=f'Fold {fold_idx+1}', alpha=0.7)\n    axes[1].plot(history['epoch'], history['valid_loss'], label=f'Fold {fold_idx+1}', alpha=0.7)\n    axes[2].plot(history['epoch'], history['train_dice'], label=f'Fold {fold_idx+1}', alpha=0.7)\n    axes[3].plot(history['epoch'], history['valid_dice'], label=f'Fold {fold_idx+1}', alpha=0.7)\n\naxes[0].set_title('Training Loss')\naxes[0].set_xlabel('Epoch')\naxes[0].set_ylabel('Loss')\naxes[0].legend()\naxes[0].grid(True, alpha=0.3)\n\naxes[1].set_title('Validation Loss')\naxes[1].set_xlabel('Epoch')\naxes[1].set_ylabel('Loss')\naxes[1].legend()\naxes[1].grid(True, alpha=0.3)\n\naxes[2].set_title('Training Dice Score')\naxes[2].set_xlabel('Epoch')\naxes[2].set_ylabel('Dice Score')\naxes[2].legend()\naxes[2].grid(True, alpha=0.3)\n\naxes[3].set_title('Validation Dice Score')\naxes[3].set_xlabel('Epoch')\naxes[3].set_ylabel('Dice Score')\naxes[3].legend()\naxes[3].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()","metadata":{"papermill":{"duration":0.904609,"end_time":"2025-11-12T09:06:35.904384","exception":false,"start_time":"2025-11-12T09:06:34.999775","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}