{"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":"gpu","dataSources":[{"sourceId":39272,"databundleVersionId":4629629,"sourceType":"competition"},{"sourceId":1873742,"sourceType":"datasetVersion","datasetId":1115384},{"sourceId":7133048,"sourceType":"datasetVersion","datasetId":4115648},{"sourceId":12237999,"sourceType":"datasetVersion","datasetId":7710801}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport glob\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow.keras import *\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\nfrom PIL import Image\nimport math\n\nPI = math.pi","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:04.926698Z","iopub.execute_input":"2026-01-27T21:07:04.927221Z","iopub.status.idle":"2026-01-27T21:07:19.120993Z","shell.execute_reply.started":"2026-01-27T21:07:04.927191Z","shell.execute_reply":"2026-01-27T21:07:19.12042Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TẠO DATASET","metadata":{}},{"cell_type":"code","source":"!rm -rf /kaggle/working/cbis_ddsm_roi\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:19.122168Z","iopub.execute_input":"2026-01-27T21:07:19.122633Z","iopub.status.idle":"2026-01-27T21:07:19.252426Z","shell.execute_reply.started":"2026-01-27T21:07:19.12261Z","shell.execute_reply":"2026-01-27T21:07:19.251621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_ROOT = \"/kaggle/input/cbis-ddsm\"\nOUT_ROOT  = \"/kaggle/working/cbis_ddsm_roi\"\n\nIMG_SIZE_FULL = (1024, 1024)\nIMG_SIZE_ROI  = (224, 224)\nROI_SCALE = 1.3","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:19.253502Z","iopub.execute_input":"2026-01-27T21:07:19.253765Z","iopub.status.idle":"2026-01-27T21:07:19.257838Z","shell.execute_reply.started":"2026-01-27T21:07:19.253729Z","shell.execute_reply":"2026-01-27T21:07:19.257094Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_image_raw(path):\n    img = cv2.imread(path)\n    if img is None:\n        raise FileNotFoundError(path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    return img   # uint8 [0–255]\n\ndef load_image(path, size=None):\n    img = cv2.imread(path)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    if size is not None:\n        img = cv2.resize(img, size)\n    img = img.astype(np.float32) / 255.0   # ✅ CHỈ DÙNG KHI TRAIN\n    return img\n\n\ndef load_yolo_label(path):\n    \"\"\"\n    Return: class_id, bbox(cx, cy, w, h)\n    \"\"\"\n    if not os.path.exists(path):\n        return None, None\n\n    with open(path) as f:\n        lines = f.readlines()\n\n    if len(lines) == 0:\n        return None, None\n\n    cls, cx, cy, w, h = map(float, lines[0].split())\n    return int(cls), np.array([cx, cy, w, h])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:19.259341Z","iopub.execute_input":"2026-01-27T21:07:19.259602Z","iopub.status.idle":"2026-01-27T21:07:19.273159Z","shell.execute_reply.started":"2026-01-27T21:07:19.259583Z","shell.execute_reply":"2026-01-27T21:07:19.272548Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def yolo_to_pixel_bbox(bbox, img_w, img_h):\n    cx, cy, w, h = bbox\n    x1 = int((cx - w / 2) * img_w)\n    y1 = int((cy - h / 2) * img_h)\n    x2 = int((cx + w / 2) * img_w)\n    y2 = int((cy + h / 2) * img_h)\n    return x1, y1, x2, y2\n\n\ndef crop_roi(img, bbox, scale=1.3):\n    h, w, _ = img.shape\n    x1, y1, x2, y2 = yolo_to_pixel_bbox(bbox, w, h)\n\n    bw = x2 - x1\n    bh = y2 - y1\n    cx = (x1 + x2) // 2\n    cy = (y1 + y2) // 2\n\n    bw = int(bw * scale)\n    bh = int(bh * scale)\n\n    x1 = max(cx - bw // 2, 0)\n    y1 = max(cy - bh // 2, 0)\n    x2 = min(cx + bw // 2, w)\n    y2 = min(cy + bh // 2, h)\n\n    if x2 <= x1 or y2 <= y1:\n        return None\n\n    return img[y1:y2, x1:x2]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:19.273981Z","iopub.execute_input":"2026-01-27T21:07:19.274265Z","iopub.status.idle":"2026-01-27T21:07:19.286401Z","shell.execute_reply.started":"2026-01-27T21:07:19.274236Z","shell.execute_reply":"2026-01-27T21:07:19.28581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_view_type(img_name):\n    name = img_name.lower()\n    if \"cc\" in name:\n        return \"cc\"\n    elif \"mlo\" in name:\n        return \"mlo\"\n    else:\n        return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:19.287095Z","iopub.execute_input":"2026-01-27T21:07:19.287347Z","iopub.status.idle":"2026-01-27T21:07:19.299666Z","shell.execute_reply.started":"2026-01-27T21:07:19.28732Z","shell.execute_reply":"2026-01-27T21:07:19.299086Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_dirs(split, view):\n    base = f\"{OUT_ROOT}/{split}/{view}\"\n    os.makedirs(f\"{base}/images_full\", exist_ok=True)\n    os.makedirs(f\"{base}/images_roi\", exist_ok=True)\n    return base","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:19.300388Z","iopub.execute_input":"2026-01-27T21:07:19.300623Z","iopub.status.idle":"2026-01-27T21:07:19.316408Z","shell.execute_reply.started":"2026-01-27T21:07:19.300602Z","shell.execute_reply":"2026-01-27T21:07:19.315711Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for split in [\"train\", \"valid\", \"test\"]:\n    print(f\"\\nProcessing {split}...\")\n    img_paths = sorted(glob.glob(f\"{DATA_ROOT}/{split}/images/*.jpg\"))\n\n    records = {\"cc\": [], \"mlo\": []}\n\n    for img_path in tqdm(img_paths):\n        img_name = os.path.basename(img_path)\n        view = get_view_type(img_name)\n        if view is None:\n            continue\n\n        label_path = f\"{DATA_ROOT}/{split}/labels/{img_name.replace('.jpg','.txt')}\"\n        cls, bbox = load_yolo_label(label_path)\n        if bbox is None:\n            continue\n\n        img = load_image_raw(img_path)\n\n        # crop ROI\n        roi = crop_roi(img, bbox)\n        if roi is None or roi.size == 0:   # ← الزوج تعديلات هنا\n            continue\n\n        base = make_dirs(split, view)\n\n        # resize\n        img_full = cv2.resize(img, IMG_SIZE_FULL)\n        img_roi  = cv2.resize(roi, IMG_SIZE_ROI)\n\n        # save images\n        cv2.imwrite(\n            f\"{base}/images_full/{img_name}\",\n            cv2.cvtColor(img_full, cv2.COLOR_RGB2BGR)\n        )\n        cv2.imwrite(\n            f\"{base}/images_roi/{img_name}\",\n            cv2.cvtColor(img_roi, cv2.COLOR_RGB2BGR)\n        )\n\n        # record\n        records[view].append({\n            \"image\": img_name,\n            \"class\": cls,\n            \"bbox\": bbox.tolist()\n        })\n\n    # save CSV\n    for view in [\"cc\", \"mlo\"]:\n        df = pd.DataFrame(records[view])\n        df.to_csv(f\"{OUT_ROOT}/{split}/{view}/labels.csv\", index=False)\n\n       ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:07:19.317241Z","iopub.execute_input":"2026-01-27T21:07:19.317779Z","iopub.status.idle":"2026-01-27T21:10:17.278065Z","shell.execute_reply.started":"2026-01-27T21:07:19.317759Z","shell.execute_reply":"2026-01-27T21:10:17.27745Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"p = \"/kaggle/working/cbis_ddsm_roi/test/mlo/images_full/Mass-Training_P_00023_RIGHT_MLO_jpg.rf.81de62ad1ba7506fc01cd8741fe17025.jpg\"\n\nimg = cv2.imread(p)\nif img is None:\n    raise FileNotFoundError(p)\n\nprint(img.dtype, img.min(), img.max())\n\nplt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))\nplt.axis(\"off\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.278942Z","iopub.execute_input":"2026-01-27T21:10:17.279214Z","iopub.status.idle":"2026-01-27T21:10:17.4974Z","shell.execute_reply.started":"2026-01-27T21:10:17.279189Z","shell.execute_reply":"2026-01-27T21:10:17.496717Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"p1 = \"/kaggle/working/cbis_ddsm_roi/test/mlo/images_roi/Mass-Training_P_00023_RIGHT_MLO_jpg.rf.81de62ad1ba7506fc01cd8741fe17025.jpg\"\nimg1 = cv2.imread(p1)\nprint(img1.dtype, img1.min(), img1.max())\n\nplt.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB))\nplt.axis(\"off\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.499504Z","iopub.execute_input":"2026-01-27T21:10:17.499781Z","iopub.status.idle":"2026-01-27T21:10:17.571768Z","shell.execute_reply.started":"2026-01-27T21:10:17.49976Z","shell.execute_reply":"2026-01-27T21:10:17.571181Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test1_df = pd.read_csv(f\"{OUT_ROOT}/train/cc/labels.csv\")\nprint(test1_df.head())\n\nimg = load_image(f\"{OUT_ROOT}/train/cc/images_full/{test1_df.iloc[0].image}\")\nroi = load_image(f\"{OUT_ROOT}/train/cc/images_roi/{test1_df.iloc[0].image}\")\n\nprint(\"Full:\", img.shape, \"ROI:\", roi.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.572585Z","iopub.execute_input":"2026-01-27T21:10:17.572781Z","iopub.status.idle":"2026-01-27T21:10:17.608261Z","shell.execute_reply.started":"2026-01-27T21:10:17.572762Z","shell.execute_reply":"2026-01-27T21:10:17.607536Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# HIỆU CHỈNH DATA CHO TRAINING","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport ast\n\ndef fix_bbox_csv(csv_path):\n    df = pd.read_csv(csv_path)\n\n    if \"bbox\" not in df.columns:\n        print(f\"Skip {csv_path} (no bbox column)\")\n        return\n\n    def parse_bbox(b):\n        if isinstance(b, str):\n            # an toàn nhất: parse list string\n            return list(map(float, ast.literal_eval(b)))\n        elif isinstance(b, (list, tuple)):\n            return list(b)\n        else:\n            return None\n\n    bbox = df[\"bbox\"].apply(parse_bbox)\n\n    df[\"x\"] = bbox.apply(lambda x: x[0] if x is not None else None)\n    df[\"y\"] = bbox.apply(lambda x: x[1] if x is not None else None)\n    df[\"w\"] = bbox.apply(lambda x: x[2] if x is not None else None)\n    df[\"h\"] = bbox.apply(lambda x: x[3] if x is not None else None)\n\n    df = df.drop(columns=[\"bbox\"])\n    df.to_csv(csv_path, index=False)\n    print(f\"Fixed {csv_path}\")\n\n\n\nROOT = \"/kaggle/working/cbis_ddsm_roi\"\n\nfor split in [\"train\", \"valid\", \"test\"]:\n    for view in [\"cc\", \"mlo\"]:\n        csv_path = f\"{ROOT}/{split}/{view}/labels.csv\"\n        fix_bbox_csv(csv_path)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.609442Z","iopub.execute_input":"2026-01-27T21:10:17.610083Z","iopub.status.idle":"2026-01-27T21:10:17.772425Z","shell.execute_reply.started":"2026-01-27T21:10:17.610056Z","shell.execute_reply":"2026-01-27T21:10:17.771693Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test2_df = pd.read_csv(f\"{OUT_ROOT}/train/cc/labels.csv\")\nprint(test2_df.columns)\ntest2_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.773401Z","iopub.execute_input":"2026-01-27T21:10:17.773761Z","iopub.status.idle":"2026-01-27T21:10:17.796577Z","shell.execute_reply.started":"2026-01-27T21:10:17.773725Z","shell.execute_reply":"2026-01-27T21:10:17.795915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CBISMultiViewDataset(tf.keras.utils.Sequence):\n    def __init__(self, root, split, batch_size=4, shuffle=True, **kwargs):\n        super().__init__(**kwargs)\n        \n        self.batch_size = batch_size\n        self.shuffle = shuffle\n\n        self.cc_df  = pd.read_csv(f\"{root}/{split}/cc/labels.csv\")\n        self.mlo_df = pd.read_csv(f\"{root}/{split}/mlo/labels.csv\")\n\n        self.length = min(len(self.cc_df), len(self.mlo_df))\n        self.indices = np.arange(self.length)\n\n        self.cc_full_dir  = f\"{root}/{split}/cc/images_full\"\n        self.cc_roi_dir   = f\"{root}/{split}/cc/images_roi\"\n        self.mlo_full_dir = f\"{root}/{split}/mlo/images_full\"\n        self.mlo_roi_dir  = f\"{root}/{split}/mlo/images_roi\"\n\n        self.on_epoch_end()\n        print(f\"[{split}] paired samples:\", self.length)\n\n    def __len__(self):\n        return int(np.ceil(self.length / self.batch_size))  # <-- تصحيح\n\n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n\n    def __getitem__(self, idx):\n        batch_idx = self.indices[idx*self.batch_size:(idx+1)*self.batch_size]\n\n        cc_full, cc_roi, mlo_full, mlo_roi = [], [], [], []\n        cls, bbox = [], []\n\n        for i in batch_idx:\n            cc_row  = self.cc_df.iloc[i]\n            mlo_row = self.mlo_df.iloc[i]\n\n            cc_full.append(load_image(f\"{self.cc_full_dir}/{cc_row.image}\", (512,512)))\n            cc_roi.append(load_image(f\"{self.cc_roi_dir}/{cc_row.image}\", (224,224)))\n\n            mlo_full.append(load_image(f\"{self.mlo_full_dir}/{mlo_row.image}\", (512,512)))\n            mlo_roi.append(load_image(f\"{self.mlo_roi_dir}/{mlo_row.image}\", (224,224)))\n\n            cls.append(cc_row[\"class\"])\n            bbox.append(cc_row[[\"x\",\"y\",\"w\",\"h\"]].values.astype(np.float32))\n\n        return (\n            {\n                \"cc_full\":  np.array(cc_full),\n                \"cc_roi\":   np.array(cc_roi),\n                \"mlo_full\": np.array(mlo_full),\n                \"mlo_roi\":  np.array(mlo_roi),\n            },\n            {\n                \"cancer_cls\": np.array(cls),\n                \"bbox\":       np.array(bbox),\n            }\n        )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.797408Z","iopub.execute_input":"2026-01-27T21:10:17.797659Z","iopub.status.idle":"2026-01-27T21:10:17.806328Z","shell.execute_reply.started":"2026-01-27T21:10:17.797638Z","shell.execute_reply":"2026-01-27T21:10:17.805612Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = CBISMultiViewDataset(\n    root=\"/kaggle/working/cbis_ddsm_roi\",\n    split=\"train\",\n    batch_size=8,\n    shuffle=True\n)\n\nval_ds = CBISMultiViewDataset(\n    root=\"/kaggle/working/cbis_ddsm_roi\",\n    split=\"valid\",\n    batch_size=8,\n    shuffle=False\n)\n  ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.807332Z","iopub.execute_input":"2026-01-27T21:10:17.807567Z","iopub.status.idle":"2026-01-27T21:10:17.836674Z","shell.execute_reply.started":"2026-01-27T21:10:17.807547Z","shell.execute_reply":"2026-01-27T21:10:17.836164Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"train:\")\nx_train, y_train = train_ds[0]\n\nfor k, v in x_train.items():\n    print(k, v.shape)\n\nfor k, v in y_train.items():\n    print(k, v.shape)\n\nprint(\"\\nvalidation:\")\nx_val, y_val = val_ds[0]\n\nfor k, v in x_val.items():\n    print(k, v.shape)\n\nfor k, v in y_val.items():\n    print(k, v.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:17.837618Z","iopub.execute_input":"2026-01-27T21:10:17.837925Z","iopub.status.idle":"2026-01-27T21:10:18.13599Z","shell.execute_reply.started":"2026-01-27T21:10:17.837865Z","shell.execute_reply":"2026-01-27T21:10:18.135135Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# MODEL","metadata":{}},{"cell_type":"code","source":"EPS = 1e-7\nimport math\nPI = math.pi\n\ndef cxcywh_to_xyxy(box):\n    cx, cy, w, h = tf.split(box, 4, axis=-1)\n    x1 = cx - w / 2\n    y1 = cy - h / 2\n    x2 = cx + w / 2\n    y2 = cy + h / 2\n    return tf.concat([x1, y1, x2, y2], axis=-1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.136995Z","iopub.execute_input":"2026-01-27T21:10:18.137319Z","iopub.status.idle":"2026-01-27T21:10:18.141729Z","shell.execute_reply.started":"2026-01-27T21:10:18.137276Z","shell.execute_reply":"2026-01-27T21:10:18.141033Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_full_backbone(input_shape):\n    base = tf.keras.applications.ResNet50(\n        include_top=False,\n        weights=\"imagenet\",\n        input_shape=input_shape\n    )\n    for l in base.layers:\n        l.trainable = False\n\n    c3 = base.get_layer(\"conv3_block4_out\").output\n    c4 = base.get_layer(\"conv4_block6_out\").output\n    c5 = base.get_layer(\"conv5_block3_out\").output\n\n    p5 = layers.Conv2D(256, 1, name=\"p5\")(c5)\n    p4 = layers.Add()([\n        layers.UpSampling2D()(p5),\n        layers.Conv2D(256, 1)(c4)\n    ])\n    p3 = layers.Add()([\n        layers.UpSampling2D()(p4),\n        layers.Conv2D(256, 1)(c3)\n    ])\n\n    return models.Model(base.input, [p3, p4, p5], name=\"Full_Backbone\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.142582Z","iopub.execute_input":"2026-01-27T21:10:18.142792Z","iopub.status.idle":"2026-01-27T21:10:18.156875Z","shell.execute_reply.started":"2026-01-27T21:10:18.142773Z","shell.execute_reply":"2026-01-27T21:10:18.156418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_roi_backbone(input_shape):\n    base = tf.keras.applications.EfficientNetB0(\n        include_top=False,\n        weights=\"imagenet\",\n        input_shape=input_shape\n    )\n    for l in base.layers:\n        l.trainable = False\n\n    x = layers.GlobalAveragePooling2D()(base.output)\n    x = layers.Dense(256, activation=\"relu\")(x)\n\n    return models.Model(base.input, x, name=\"ROI_Backbone\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.157764Z","iopub.execute_input":"2026-01-27T21:10:18.158115Z","iopub.status.idle":"2026-01-27T21:10:18.16882Z","shell.execute_reply.started":"2026-01-27T21:10:18.158087Z","shell.execute_reply":"2026-01-27T21:10:18.168166Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def multiscale_head(p3, p4, p5, name):\n    def block(x):\n        x = layers.Conv2D(256, 1, activation=\"relu\")(x)\n        return layers.GlobalAveragePooling2D()(x)\n\n    f3 = block(p3)\n    f4 = block(p4)\n    f5 = block(p5)\n\n    x = layers.Concatenate(name=name)([f3, f4, f5])\n    x = layers.Dense(256, activation=\"relu\")(x)\n    return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.169783Z","iopub.execute_input":"2026-01-27T21:10:18.169993Z","iopub.status.idle":"2026-01-27T21:10:18.184607Z","shell.execute_reply.started":"2026-01-27T21:10:18.169974Z","shell.execute_reply":"2026-01-27T21:10:18.184096Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def cross_view_gate(cc, mlo):\n    channels = cc.shape[-1]\n    if channels is None:\n        raise ValueError(\"cc.shape[-1] is None. Make sure the input tensor has a known channel dimension.\")\n\n    gate = layers.Dense(int(channels), activation=\"sigmoid\")(cc + mlo)\n    cc  = layers.Multiply()([cc, gate])\n    mlo = layers.Multiply()([mlo, gate])\n    return cc, mlo\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.185551Z","iopub.execute_input":"2026-01-27T21:10:18.186058Z","iopub.status.idle":"2026-01-27T21:10:18.199151Z","shell.execute_reply.started":"2026-01-27T21:10:18.186037Z","shell.execute_reply":"2026-01-27T21:10:18.198621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ViewConsistencyLayer(layers.Layer):\n    def __init__(self, weight=0.2):\n        super().__init__()\n        self.weight = weight\n\n    def call(self, cc, mlo):\n        loss = tf.reduce_mean(tf.square(cc - mlo))\n        self.add_loss(self.weight * loss)\n        return cc, mlo","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.20012Z","iopub.execute_input":"2026-01-27T21:10:18.200555Z","iopub.status.idle":"2026-01-27T21:10:18.214009Z","shell.execute_reply.started":"2026-01-27T21:10:18.200528Z","shell.execute_reply":"2026-01-27T21:10:18.213344Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CrossViewAttention(tf.keras.layers.Layer):\n    def __init__(self, dim=256, **kwargs):\n        super().__init__(**kwargs)\n        self.dim = dim\n        self.q = tf.keras.layers.Dense(dim)\n        self.k = tf.keras.layers.Dense(dim)\n        self.v = tf.keras.layers.Dense(dim)\n        self.softmax = tf.keras.layers.Softmax(axis=-1)\n\n    def call(self, x, y):\n        q = self.q(x)\n        k = self.k(y)\n        v = self.v(y)\n        attn = self.softmax(tf.matmul(q, k, transpose_b=True))\n        return tf.matmul(attn, v)\n\n    def get_config(self):\n        cfg = super().get_config()\n        cfg.update({\"dim\": self.dim})\n        return cfg","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.214935Z","iopub.execute_input":"2026-01-27T21:10:18.215183Z","iopub.status.idle":"2026-01-27T21:10:18.227619Z","shell.execute_reply.started":"2026-01-27T21:10:18.215154Z","shell.execute_reply":"2026-01-27T21:10:18.227034Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_final_model():\n    # ===== Inputs =====\n    cc_full  = layers.Input((512,512,3), name=\"cc_full\")\n    cc_roi   = layers.Input((224,224,3), name=\"cc_roi\")\n    mlo_full = layers.Input((512,512,3), name=\"mlo_full\")\n    mlo_roi  = layers.Input((224,224,3), name=\"mlo_roi\")\n\n    # ===== Backbones =====\n    full_net = build_full_backbone((512,512,3))\n    roi_net  = build_roi_backbone((224,224,3))\n\n    # ===== FULL features =====\n    cc_p3, cc_p4, cc_p5   = full_net(cc_full)\n    mlo_p3, mlo_p4, mlo_p5 = full_net(mlo_full)\n\n    # ===== ROI features =====\n    cc_roi_f  = roi_net(cc_roi)\n    mlo_roi_f = roi_net(mlo_roi)\n\n    # ===== Multi-scale =====\n    cc_feat  = multiscale_head(cc_p3, cc_p4, cc_p5, \"cc_multiscale\")\n    mlo_feat = multiscale_head(mlo_p3, mlo_p4, mlo_p5, \"mlo_multiscale\")\n\n    # ===== Inject ROI =====\n    cc_feat  = layers.Concatenate()([cc_feat, cc_roi_f])\n    mlo_feat = layers.Concatenate()([mlo_feat, mlo_roi_f])\n\n    cc_feat  = layers.Dense(256, activation=\"relu\")(cc_feat)\n    mlo_feat = layers.Dense(256, activation=\"relu\")(mlo_feat)\n\n    # ===== Cross-view gate =====\n    cc_feat, mlo_feat = cross_view_gate(cc_feat, mlo_feat)\n\n    # ===== View consistency =====\n    cc_feat, mlo_feat = ViewConsistencyLayer(0.2)(cc_feat, mlo_feat)\n\n    # ===== Fusion =====\n    fused = layers.Concatenate()([cc_feat, mlo_feat])\n    fused = layers.Dense(512, activation=\"relu\")(fused)\n    fused = layers.Dropout(0.3)(fused)\n\n    # ===== Heads =====\n    cls = layers.Dense(1, activation=\"sigmoid\", name=\"cancer_cls\")(fused)\n    \n    bbox_raw = layers.Dense(4,activation=\"sigmoid\",)(fused)\n    bbox_out = layers.Lambda(cxcywh_to_xyxy,name=\"bbox\")(bbox_raw)\n\n\n    return models.Model(\n        inputs={\n            \"cc_full\": cc_full,\n            \"cc_roi\": cc_roi,\n            \"mlo_full\": mlo_full,\n            \"mlo_roi\": mlo_roi\n        },\n        outputs={\n            \"cancer_cls\": cls,\n            \"bbox\": bbox_out\n        },\n        name=\"MultiView_CBIS\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.228418Z","iopub.execute_input":"2026-01-27T21:10:18.228648Z","iopub.status.idle":"2026-01-27T21:10:18.243795Z","shell.execute_reply.started":"2026-01-27T21:10:18.228627Z","shell.execute_reply":"2026-01-27T21:10:18.243312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = build_final_model()\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:18.244626Z","iopub.execute_input":"2026-01-27T21:10:18.244841Z","iopub.status.idle":"2026-01-27T21:10:23.710686Z","shell.execute_reply.started":"2026-01-27T21:10:18.244822Z","shell.execute_reply":"2026-01-27T21:10:23.709983Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TRAINING","metadata":{}},{"cell_type":"markdown","source":"**Prepare**","metadata":{}},{"cell_type":"code","source":"def biou(y_true, y_pred):\n    b1 = cxcywh_to_xyxy(y_true)\n    b2 = cxcywh_to_xyxy(y_pred)\n\n    x1 = tf.maximum(b1[..., 0], b2[..., 0])\n    y1 = tf.maximum(b1[..., 1], b2[..., 1])\n    x2 = tf.minimum(b1[..., 2], b2[..., 2])\n    y2 = tf.minimum(b1[..., 3], b2[..., 3])\n\n    inter = tf.maximum(x2 - x1, 0) * tf.maximum(y2 - y1, 0)\n\n    area1 = (b1[..., 2] - b1[..., 0]) * (b1[..., 3] - b1[..., 1])\n    area2 = (b2[..., 2] - b2[..., 0]) * (b2[..., 3] - b2[..., 1])\n\n    union = area1 + area2 - inter + EPS\n    return tf.reduce_mean(inter / union)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:23.711679Z","iopub.execute_input":"2026-01-27T21:10:23.711967Z","iopub.status.idle":"2026-01-27T21:10:23.717466Z","shell.execute_reply.started":"2026-01-27T21:10:23.711943Z","shell.execute_reply":"2026-01-27T21:10:23.716934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def b_ciou(y_true, y_pred):\n    b1 = cxcywh_to_xyxy(y_true)\n    b2 = cxcywh_to_xyxy(y_pred)\n\n    x1 = tf.maximum(b1[..., 0], b2[..., 0])\n    y1 = tf.maximum(b1[..., 1], b2[..., 1])\n    x2 = tf.minimum(b1[..., 2], b2[..., 2])\n    y2 = tf.minimum(b1[..., 3], b2[..., 3])\n\n    inter = tf.maximum(x2 - x1, 0) * tf.maximum(y2 - y1, 0)\n\n    area1 = (b1[..., 2] - b1[..., 0]) * (b1[..., 3] - b1[..., 1])\n    area2 = (b2[..., 2] - b2[..., 0]) * (b2[..., 3] - b2[..., 1])\n    union = area1 + area2 - inter + EPS\n\n    iou = inter / union\n\n    # center distance\n    c1x = (b1[..., 0] + b1[..., 2]) / 2\n    c1y = (b1[..., 1] + b1[..., 3]) / 2\n    c2x = (b2[..., 0] + b2[..., 2]) / 2\n    c2y = (b2[..., 1] + b2[..., 3]) / 2\n\n    center_dist = tf.square(c1x - c2x) + tf.square(c1y - c2y)\n\n    # enclosing box\n    enc_x1 = tf.minimum(b1[..., 0], b2[..., 0])\n    enc_y1 = tf.minimum(b1[..., 1], b2[..., 1])\n    enc_x2 = tf.maximum(b1[..., 2], b2[..., 2])\n    enc_y2 = tf.maximum(b1[..., 3], b2[..., 3])\n\n    enc_diag = tf.square(enc_x2 - enc_x1) + tf.square(enc_y2 - enc_y1) + EPS\n\n    # aspect ratio penalty\n    w1 = b1[..., 2] - b1[..., 0]\n    h1 = b1[..., 3] - b1[..., 1]\n    w2 = b2[..., 2] - b2[..., 0]\n    h2 = b2[..., 3] - b2[..., 1]\n\n    v = (4.0 / (PI ** 2)) * tf.square(\n        tf.atan(w1 / (h1 + EPS)) - tf.atan(w2 / (h2 + EPS))\n    )\n\n    alpha = v / (1 - iou + v + EPS)\n\n    ciou = iou - center_dist / enc_diag - alpha * v\n    return ciou","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:23.718466Z","iopub.execute_input":"2026-01-27T21:10:23.71889Z","iopub.status.idle":"2026-01-27T21:10:23.738788Z","shell.execute_reply.started":"2026-01-27T21:10:23.718863Z","shell.execute_reply":"2026-01-27T21:10:23.738333Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def ciou_loss(y_true, y_pred):\n    return 1.0 - b_ciou(y_true, y_pred)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:23.742011Z","iopub.execute_input":"2026-01-27T21:10:23.742265Z","iopub.status.idle":"2026-01-27T21:10:23.750673Z","shell.execute_reply.started":"2026-01-27T21:10:23.742245Z","shell.execute_reply":"2026-01-27T21:10:23.750052Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BBoxCIoULoss(tf.keras.losses.Loss):\n    def __init__(self, l1_weight=1.0, ciou_weight=1.0, name=\"bbox_ciou_loss\"):\n        super().__init__(name=name)\n        self.l1_weight = l1_weight\n        self.ciou_weight = ciou_weight\n\n    def call(self, y_true, y_pred):\n        l1 = tf.reduce_mean(tf.abs(y_true - y_pred))\n\n        ciou_score = b_ciou(y_true, y_pred)   # لازم ترجع scalar\n        ciou_loss = 1.0 - ciou_score\n\n        return self.l1_weight * l1 + self.ciou_weight * ciou_loss\n\n    def get_config(self):\n        return {\n            \"l1_weight\": self.l1_weight,\n            \"ciou_weight\": self.ciou_weight,\n            \"name\": self.name\n        }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:23.751374Z","iopub.execute_input":"2026-01-27T21:10:23.751584Z","iopub.status.idle":"2026-01-27T21:10:23.766068Z","shell.execute_reply.started":"2026-01-27T21:10:23.751565Z","shell.execute_reply":"2026-01-27T21:10:23.765494Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Train**","metadata":{}},{"cell_type":"markdown","source":"Warm up 10 epochs","metadata":{}},{"cell_type":"code","source":"model.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-4),\n    loss={\n        \"cancer_cls\": tf.keras.losses.BinaryCrossentropy(),\n        \"bbox\": tf.keras.losses.Huber(delta=0.1),\n    },\n    loss_weights={\n        \"cancer_cls\": 1.0,\n        \"bbox\": 1.0,\n    },\n    metrics={\n        \"cancer_cls\": [\"accuracy\"],\n        \"bbox\": [biou],\n    },\n    jit_compile=False\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:23.766871Z","iopub.execute_input":"2026-01-27T21:10:23.767172Z","iopub.status.idle":"2026-01-27T21:10:23.788999Z","shell.execute_reply.started":"2026-01-27T21:10:23.767151Z","shell.execute_reply":"2026-01-27T21:10:23.788534Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"warmup_ckpt = tf.keras.callbacks.ModelCheckpoint(\n    \"weights_warmup.weights.h5\",\n    monitor=\"val_loss\",\n    mode=\"min\",\n    save_best_only=True,\n    save_weights_only=True,\n    verbose=1\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:23.789806Z","iopub.execute_input":"2026-01-27T21:10:23.790054Z","iopub.status.idle":"2026-01-27T21:10:23.793711Z","shell.execute_reply.started":"2026-01-27T21:10:23.790035Z","shell.execute_reply":"2026-01-27T21:10:23.793068Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=10,\n    callbacks=[warmup_ckpt]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:10:23.794442Z","iopub.execute_input":"2026-01-27T21:10:23.794612Z","iopub.status.idle":"2026-01-27T21:24:14.623813Z","shell.execute_reply.started":"2026-01-27T21:10:23.794595Z","shell.execute_reply":"2026-01-27T21:24:14.623182Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_weights(\"weights_warmup.weights.h5\")\n\nx, _ = train_ds[0]\npred = model(x, training=False)[\"bbox\"].numpy()\n\nprint(\"bbox range:\", pred.min(), pred.max())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:24:14.630144Z","iopub.execute_input":"2026-01-27T21:24:14.630639Z","iopub.status.idle":"2026-01-27T21:24:16.960261Z","shell.execute_reply.started":"2026-01-27T21:24:14.630615Z","shell.execute_reply":"2026-01-27T21:24:16.959385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_weights(\"weights_warmup.weights.h5\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:24:16.961389Z","iopub.execute_input":"2026-01-27T21:24:16.961768Z","iopub.status.idle":"2026-01-27T21:24:18.084638Z","shell.execute_reply.started":"2026-01-27T21:24:16.961738Z","shell.execute_reply":"2026-01-27T21:24:18.083795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=tf.keras.optimizers.AdamW(1e-4),\n    loss={\n        \"bbox\": BBoxCIoULoss(l1_weight=1.0, ciou_weight=1.0)\n    },\n    metrics={\n        \"bbox\": [biou]\n    }\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:24:18.085587Z","iopub.execute_input":"2026-01-27T21:24:18.085949Z","iopub.status.idle":"2026-01-27T21:24:18.25153Z","shell.execute_reply.started":"2026-01-27T21:24:18.085926Z","shell.execute_reply":"2026-01-27T21:24:18.250252Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"callbacks = [\n    tf.keras.callbacks.ModelCheckpoint(\n        filepath=\"best_model.keras\",    \n        monitor=\"val_bbox_biou\",       \n        mode=\"max\",                   \n        save_best_only=True,               \n        save_weights_only=False,           \n        verbose=1\n    ),\n    \n    tf.keras.callbacks.ReduceLROnPlateau(\n        monitor=\"val_bbox_biou\",\n        mode=\"max\",\n        factor=0.3,\n        patience=5,\n        min_lr=1e-6,\n        verbose=1\n    ),\n    \n    tf.keras.callbacks.EarlyStopping(\n        monitor=\"val_bbox_biou\",\n        mode=\"max\",\n        patience=12,                  \n        restore_best_weights=True,     \n        verbose=1\n    )\n]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:24:18.25391Z","iopub.execute_input":"2026-01-27T21:24:18.254224Z","iopub.status.idle":"2026-01-27T21:24:18.261925Z","shell.execute_reply.started":"2026-01-27T21:24:18.254173Z","shell.execute_reply":"2026-01-27T21:24:18.261266Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1) rebuild model\nmodel = build_final_model()\n\n# 2) load weights (optional but recommended)\nmodel.load_weights(\"weights_warmup.weights.h5\")\n\n# 3) compile\nmodel.compile(\n    optimizer=tf.keras.optimizers.AdamW(1e-4),\n    loss={\n        \"bbox\": BBoxCIoULoss(l1_weight=1.0, ciou_weight=1.0),\n        \"cancer_cls\": tf.keras.losses.BinaryCrossentropy()\n    },\n    loss_weights={\n        \"bbox\": 1.0,\n        \"cancer_cls\": 1.0\n    },\n    metrics={\n        \"bbox\": [biou],\n        \"cancer_cls\": [\"accuracy\"]\n    }\n)\n\n# 4) fit\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=20,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:24:18.262789Z","iopub.execute_input":"2026-01-27T21:24:18.263232Z","iopub.status.idle":"2026-01-27T21:43:36.048023Z","shell.execute_reply.started":"2026-01-27T21:24:18.263208Z","shell.execute_reply":"2026-01-27T21:43:36.047195Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=20,\n    callbacks=callbacks\n)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T21:43:36.049153Z","iopub.execute_input":"2026-01-27T21:43:36.049452Z","iopub.status.idle":"2026-01-27T22:00:19.058704Z","shell.execute_reply.started":"2026-01-27T21:43:36.049428Z","shell.execute_reply":"2026-01-27T22:00:19.057979Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# EVALUATION","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef plot_loss(history):\n    plt.figure(figsize=(8,5))\n    plt.plot(history.history[\"loss\"], label=\"Train Loss\")\n    plt.plot(history.history[\"val_loss\"], label=\"Val Loss\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.title(\"Total Loss\")\n    plt.legend()\n    plt.grid(True)\n    plt.show()\n\nplot_loss(history)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:19.059624Z","iopub.execute_input":"2026-01-27T22:00:19.059852Z","iopub.status.idle":"2026-01-27T22:00:19.208892Z","shell.execute_reply.started":"2026-01-27T22:00:19.059829Z","shell.execute_reply":"2026-01-27T22:00:19.208342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_iou(history):\n    plt.figure(figsize=(8,5))\n    plt.plot(history.history[\"bbox_biou\"], label=\"Train IoU\")\n    plt.plot(history.history[\"val_bbox_biou\"], label=\"Val IoU\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"IoU\")\n    plt.title(\"BBox IoU\")\n    plt.legend()\n    plt.grid(True)\n    plt.show()\n\nplot_iou(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:19.2098Z","iopub.execute_input":"2026-01-27T22:00:19.210096Z","iopub.status.idle":"2026-01-27T22:00:19.355146Z","shell.execute_reply.started":"2026-01-27T22:00:19.210064Z","shell.execute_reply":"2026-01-27T22:00:19.354599Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_lr(history):\n    if \"learning_rate\" not in history.history:\n        print(\"No LR history found\")\n        return\n\n    plt.figure(figsize=(8,5))\n    plt.plot(history.history[\"learning_rate\"])\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Learning Rate\")\n    plt.title(\"Learning Rate Schedule\")\n    plt.grid(True)\n    plt.show()\n\nplot_lr(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:19.356049Z","iopub.execute_input":"2026-01-27T22:00:19.356315Z","iopub.status.idle":"2026-01-27T22:00:19.486525Z","shell.execute_reply.started":"2026-01-27T22:00:19.356269Z","shell.execute_reply":"2026-01-27T22:00:19.485959Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TESTING","metadata":{}},{"cell_type":"code","source":"test_ds = CBISMultiViewDataset(\n    root=\"/kaggle/working/cbis_ddsm_roi\",\n    split=\"test\",\n    batch_size=4,\n    shuffle=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:19.487369Z","iopub.execute_input":"2026-01-27T22:00:19.487624Z","iopub.status.idle":"2026-01-27T22:00:19.49519Z","shell.execute_reply.started":"2026-01-27T22:00:19.487602Z","shell.execute_reply":"2026-01-27T22:00:19.49452Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"results = model.evaluate(\n    test_ds,\n    return_dict=True,\n    verbose=1\n)\n\nfor k, v in results.items():\n    print(f\"{k}: {v:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:19.496183Z","iopub.execute_input":"2026-01-27T22:00:19.496432Z","iopub.status.idle":"2026-01-27T22:00:34.07389Z","shell.execute_reply.started":"2026-01-27T22:00:19.496411Z","shell.execute_reply":"2026-01-27T22:00:34.073342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = model.predict(test_ds, verbose=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:34.074837Z","iopub.execute_input":"2026-01-27T22:00:34.075133Z","iopub.status.idle":"2026-01-27T22:00:53.872516Z","shell.execute_reply.started":"2026-01-27T22:00:34.075102Z","shell.execute_reply":"2026-01-27T22:00:53.871878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def cxcywh_to_xyxy_np(b, W, H):\n    cx, cy, w, h = b\n    x1 = int((cx - w/2) * W)\n    y1 = int((cy - h/2) * H)\n    x2 = int((cx + w/2) * W)\n    y2 = int((cy + h/2) * H)\n    return x1, y1, x2, y2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:53.87336Z","iopub.execute_input":"2026-01-27T22:00:53.873595Z","iopub.status.idle":"2026-01-27T22:00:53.878154Z","shell.execute_reply.started":"2026-01-27T22:00:53.873565Z","shell.execute_reply":"2026-01-27T22:00:53.877589Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport random as py_random\n\ndef convert_bbox_to_pixels(b, W, H):\n    cx, cy, w, h = b\n    x1 = int((cx - w / 2) * W)\n    y1 = int((cy - h / 2) * H)\n    x2 = int((cx + w / 2) * W)\n    y2 = int((cy + h / 2) * H)\n    return x1, y1, x2, y2\n\ndef visualize_test_samples(model, test_ds, n=10):\n    indices = py_random.sample(range(len(test_ds)), n)\n\n    for idx in indices:\n        x, y_true = test_ds[idx]\n        y_pred = model.predict(x, verbose=0)\n\n        imgs = x[\"cc_full\"]\n        gt_bbox = y_true[\"bbox\"]\n        pr_bbox = y_pred[\"bbox\"]\n\n        for i in range(len(imgs)):\n            img = imgs[i]\n            H, W = img.shape[:2]\n\n            # تحويل bbox إلى إحداثيات بالصورة\n            gt = convert_bbox_to_pixels(gt_bbox[i], W, H)\n            pr = convert_bbox_to_pixels(pr_bbox[i], W, H)\n\n            # عرض الصورة\n            plt.figure(figsize=(6, 6))\n            plt.imshow(img)\n            plt.axis(\"off\")\n\n            # رسم bbox الحقيقية – اللون الأخضر\n            plt.gca().add_patch(\n                plt.Rectangle(\n                    (gt[0], gt[1]),  gt[2] - gt[0],\n                    gt[3] - gt[1],\n                    fill=False,\n                    edgecolor=\"lime\",\n                    linewidth=2,\n                    label=\"GT\"\n                )\n            )\n\n            # رسم bbox المتوقعة – اللون الأحمر\n            plt.gca().add_patch(\n                plt.Rectangle(\n                    (pr[0], pr[1]),\n                    pr[2] - pr[0],\n                    pr[3] - pr[1],\n                    fill=False,\n                    edgecolor=\"red\",\n                    linewidth=2,\n                    label=\"Pred\"\n                )\n            )\n\n            # إضافة الأسامي\n            plt.legend()\n            plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:53.879062Z","iopub.execute_input":"2026-01-27T22:00:53.879357Z","iopub.status.idle":"2026-01-27T22:00:53.893374Z","shell.execute_reply.started":"2026-01-27T22:00:53.879327Z","shell.execute_reply":"2026-01-27T22:00:53.892742Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_test_samples(model, test_ds, n=20)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-27T22:00:53.894225Z","iopub.execute_input":"2026-01-27T22:00:53.8946Z","iopub.status.idle":"2026-01-27T22:01:22.26101Z","shell.execute_reply.started":"2026-01-27T22:00:53.894571Z","shell.execute_reply":"2026-01-27T22:01:22.260368Z"}},"outputs":[],"execution_count":null}]}