{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":45867,"databundleVersionId":6924515,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Fast Patch Selection Algorithm","metadata":{}},{"cell_type":"code","source":"import cv2\nimport numpy as np\n# from openslide import OpenSlide\nimport openslide\nfrom pathlib import Path\nimport glob\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom multiprocessing import Pool\nfrom tqdm import tqdm\nimport os\nimport pandas as pd\nimport random\nfrom sklearn.neighbors import KernelDensity\nfrom PIL import Image\n\ndef get_sampled_points_density_proportional_KDE(points, desired_sample_size):\n    num_points = len(points)\n    if num_points <= desired_sample_size:\n        return points\n\n    points_arr = np.array(points)\n    \n    # Fit KDE model to the points\n    kde = KernelDensity(bandwidth=0.1)  # You can adjust the bandwidth\n    kde.fit(points_arr)\n    \n    # Generate samples from the KDE\n    samples = kde.sample(desired_sample_size)\n    final_sample = samples.tolist()\n\n    return final_sample\n\n\ndef RGB2HSD(X):\n    eps = np.finfo(float).eps\n    X[np.where(X==0.0)] = eps\n    \n    OD = -np.log(X / 1.0)\n    D  = np.mean(OD,3)\n    D[np.where(D==0.0)] = eps\n    \n    cx = OD[:,:,:,0] / (D) - 1.0\n    cy = (OD[:,:,:,1]-OD[:,:,:,2]) / (np.sqrt(3.0)*D)\n    \n    D = np.expand_dims(D,3)\n    cx = np.expand_dims(cx,3)\n    cy = np.expand_dims(cy,3)\n            \n    X_HSD = np.concatenate((D,cx,cy),3)\n    return X_HSD\n\n\ndef clean_thumbnail(thumbnail):\n    thumbnail_arr = np.asarray(thumbnail)\n    \n    wthumbnail = np.zeros_like(thumbnail_arr)\n    wthumbnail[:, :, :] = thumbnail_arr[:, :, :]\n\n    thumbnail_std = np.std(wthumbnail, axis=2)\n    wthumbnail[thumbnail_std<5] = (np.ones((1,3), dtype=\"uint8\")*255)\n    thumbnail_HSD = RGB2HSD( np.array([wthumbnail.astype('float32')/255.]) )[0]\n    kernel = np.ones((30,30),np.float32)/900\n    thumbnail_HSD_mean = cv2.filter2D(thumbnail_HSD[:,:,2],-1,kernel)\n    wthumbnail[thumbnail_HSD_mean<0.05] = (np.ones((1,3),dtype=\"uint8\")*255)\n    return wthumbnail\n\n                \ndef is_far_enough(new_point, existing_points, min_distance):\n    for point in existing_points:\n        if np.sqrt((new_point[0] - point[0])**2 + (new_point[1] - point[1])**2) < min_distance:\n            return False\n    return True\n\n\ndef get_patch_locations(tissue_mask, cthumbnail,  mask_hratio, mask_wratio, tissue_threshold, stride):\n    contours, mm = cv2.findContours(tissue_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    image_with_contours = cthumbnail.copy()\n    cv2.drawContours(image_with_contours, contours, -1, (0, 255, 0), 2)  # Draw contours on the image\n    \n    image_with_rectangles = cthumbnail.copy()\n    \n    # Calculate the step size for the grid based on the stride\n    step_w = int(mask_wratio * stride)\n    step_h = int(mask_hratio * stride)\n    \n    patch_locations = []\n    for contour in contours:\n        x, y, w, h = cv2.boundingRect(contour)\n        # plot the rectangles on the image_with_rectangles\n        cv2.rectangle(image_with_rectangles, (x, y), (x + w, y + h), (0, 255, 0), 2)\n        \n        if w >= mask_wratio and h >= mask_hratio:\n            for i in range(x, x + w - mask_wratio, step_w):\n                for j in range(y, y + h - mask_hratio, step_h):\n                    tissue_patch = tissue_mask[j:j + mask_hratio, i:i + mask_wratio]\n                    # if np.sum(tissue_patch) / (mask_hratio ** 2) > tissue_threshold:\n                    tissue_magnitude = np.count_nonzero(tissue_patch)/tissue_patch.size\n                    if tissue_magnitude  >= tissue_threshold:\n                        patch_locations.append(((i, j),tissue_magnitude))\n\n    return patch_locations, image_with_contours, image_with_rectangles\n\ndef process_wsi(wsi_obj, wsi_path, thumbnail_path, is_tma, output_patch_size=1000, tissue_percent=0.9, returnSamples=30, stride=1):\n    wsi_name = Path(wsi_path).stem + \".svs\"\n\n    if is_tma:\n        thumbnail = Image.open(wsi_path)\n        objective_power = 40\n    else:\n        thumbnail = Image.open(thumbnail_path)\n        objective_power = 20\n    \n    cthumbnail = clean_thumbnail(thumbnail)\n    tissue_mask = ((cthumbnail.mean(axis=2) != 255) * 255).astype(np.uint8)\n    # print(f\"the shape of tissue_mask is {tissue_mask.shape}\")\n    \n    # try:\n    #     objective_power = int(wsi_obj.properties['openslide.objective-power'])\n    # except:\n    #     objective_power = 20\n         \n    w, h = wsi_obj.dimensions\n    mask_hratio = int((tissue_mask.shape[0] / h) * output_patch_size)\n    mask_wratio = int((tissue_mask.shape[1] / w) * output_patch_size)\n    # Ensure the step size is at least 1 pixel\n    if mask_hratio == 0:\n        mask_hratio = 1\n    if mask_wratio == 0:\n        mask_wratio = 1\n    # print(f\"mask_hratio is {mask_hratio} and mask_wratio is {mask_wratio}\")\n    # estimate the mask patch size given the size of the WSI, the size of the mask, and the output patch size\n    mask_patch_size = int(output_patch_size / mask_wratio)\n    \n    Mask_to_WSI_ratioW = int(w / tissue_mask.shape[1])\n    Mask_to_WSI_ratioH = int(h / tissue_mask.shape[0])\n    \n    patch_locations, image_with_contours, image_with_rectangles = get_patch_locations(tissue_mask, cthumbnail, mask_hratio, mask_wratio, tissue_percent, stride)\n    # print(f\"initially generated {len(patch_locations)} patch locations\")\n    min_distance = mask_hratio * 2  # Minimum distance between points\n\n    filtered_patch_locations = []\n    for (x, y), _ in patch_locations:\n        if is_far_enough((x, y), filtered_patch_locations, min_distance):\n            filtered_patch_locations.append((x, y))\n\n    # print(f\"after is_far_enough there are {len(filtered_patch_locations)} patch locations\")\n    filtered_patch_locations = get_sampled_points_density_proportional_KDE(filtered_patch_locations, returnSamples)\n\n    scaled_patch_coordinates = []\n    for (x, y) in filtered_patch_locations:\n        scaled_patch_coordinates.append((int(x * Mask_to_WSI_ratioW), int(y * Mask_to_WSI_ratioH)))\n\n    return scaled_patch_coordinates\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-17T03:33:28.271803Z","iopub.execute_input":"2025-07-17T03:33:28.272183Z","iopub.status.idle":"2025-07-17T03:33:28.497597Z","shell.execute_reply.started":"2025-07-17T03:33:28.272155Z","shell.execute_reply":"2025-07-17T03:33:28.496499Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Import, Patch Selection, and Tensor Computation","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport torchvision.transforms as T\nimport openslide\n\nclass SlidePatchExtractor:\n    def __init__(self, image_id, patch_size=224, mode='train', tissue_threshold=0.9, num_patches=100):\n        \n        self.image_id = image_id\n        self.patch_size = patch_size\n        self.mode = mode\n        self.transform = T.Compose([\n            T.ToTensor(),\n            T.Resize((self.patch_size, self.patch_size), antialias=True),\n            T.Normalize(mean=[0.2585, 0.2556, 0.2506], std=[0.229, 0.224, 0.225])\n        ])\n        self.tissue_threshold = tissue_threshold\n        self.num_patches = num_patches\n        \n        # self.train_transform = T.Compose([\n        #     T.RandomHorizontalFlip(p=0.5),\n        #     T.RandomVerticalFlip(p=0.5),\n        #     T.RandomRotation(degrees=45),\n        #     T.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.1),\n        #     T.ToTensor(),\n        #     T.Resize((224, 224), antialias=True),\n        #     T.Normalize(mean=[0.2585, 0.2556, 0.2506], std=[0.229, 0.224, 0.225])\n        # ])\n\n        # Define paths for the source WSI and its thumbnail\n        self.source_path = os.path.join('/kaggle/input/UBC-OCEAN', f'{self.mode}_images', self.image_id + '.png')\n        self.thumbnail_path = os.path.join('/kaggle/input/UBC-OCEAN', f'{self.mode}_thumbnails', self.image_id + '_thumbnail.png')\n        \n        try:\n            self.slide = openslide.open_slide(self.source_path)\n        except openslide.OpenSlideError as e:\n            print(f\"Could not open slide {self.source_path}: {e}\")\n            self.patch_locations = []\n            return\n\n        self.width, self.height = self.slide.dimensions\n        self.is_tma = self.width < 5000 and self.height < 5000\n        standard_magnification = 20\n        self.objective_power = 40 if self.is_tma else 20\n        magnification_factor = self.objective_power / standard_magnification\n        self.extraction_patch_size = int(self.patch_size * magnification_factor)\n        \n        self.stride = 1 if self.is_tma else 4\n        \n        self.patch_locations = process_wsi(\n            wsi_obj=self.slide,\n            wsi_path=self.source_path,\n            thumbnail_path=self.thumbnail_path,\n            is_tma=self.is_tma,\n            output_patch_size=patch_size,\n            tissue_percent=tissue_threshold,\n            returnSamples=num_patches,\n            stride=self.stride\n        )\n    \n    def __len__(self):\n        \"\"\"Returns the number of patches found for this slide.\"\"\"\n        return len(self.patch_locations)\n    \n    def get_all_patch_tensors(self):\n        \"\"\"\n        Extracts all patches from the slide and returns them as a stacked tensor.\n        \"\"\"\n        patch_tensors = []\n        if not self.patch_locations:\n            # If no patches were found, return an empty tensor with the correct shape\n            return torch.empty((0, 3, self.patch_size, self.patch_size))\n\n        for (x, y) in self.patch_locations:\n            try:\n                patch_image = self.slide.read_region(\n                    (x, y), 0, (self.extraction_patch_size, self.extraction_patch_size)\n                ).convert('RGB')\n                \n                patch_tensor = self.transform(patch_image)\n                patch_tensors.append(patch_tensor)\n            except Exception as e:\n                print(f\"Error reading patch at ({x},{y}) for slide {self.image_id}: {e}\")\n                continue\n        \n        if not patch_tensors:\n            return torch.empty((0, 3, self.patch_size, self.patch_size))\n            \n        return torch.stack(patch_tensors)\n    \n    def get_patch(self, idx):\n        x, y = self.patch_locations[idx]\n        patch_image = self.slide.read_region((x, y), 0, (self.extraction_patch_size, self.extraction_patch_size)).convert('RGB')\n        patch_tensor = self.transform(patch_image)\n        return patch_tensor, patch_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-17T03:33:31.06455Z","iopub.execute_input":"2025-07-17T03:33:31.064878Z","iopub.status.idle":"2025-07-17T03:33:31.077983Z","shell.execute_reply.started":"2025-07-17T03:33:31.064855Z","shell.execute_reply":"2025-07-17T03:33:31.077079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport openslide\nfrom PIL import Image\nimport numpy as np\nfrom tqdm import tqdm\n\nclass UBCDataset(Dataset):\n    \"\"\"\n    The main Dataset class for loading slides and their labels.\n    \"\"\"\n    def __init__(self, dataframe, label_map, mode='train', patch_size=224, tissue_threshold=0.9, num_patches=100):\n        self.df = dataframe\n        self.label_map = label_map\n        self.mode = mode\n        self.patch_size = patch_size\n        self.tissue_threshold = tissue_threshold\n        self.num_patches = num_patches\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = str(row['image_id'])\n        \n        string_label = row['label']\n        # Use the label map to convert the string label to an integer\n        int_label = self.label_map[string_label]\n        # Create the tensor from the integer\n        label = torch.tensor(int_label, dtype=torch.long)\n\n        extractor = SlidePatchExtractor(\n            image_id=image_id,\n            mode=self.mode,\n            patch_size=self.patch_size,\n            tissue_threshold=self.tissue_threshold,\n            num_patches=self.num_patches\n        )\n        \n        patch_tensors = extractor.get_all_patch_tensors()\n\n        return {\"patches\": patch_tensors, \"label\": label, \"image_id\": image_id}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-17T03:33:36.680857Z","iopub.execute_input":"2025-07-17T03:33:36.681241Z","iopub.status.idle":"2025-07-17T03:33:36.689177Z","shell.execute_reply.started":"2025-07-17T03:33:36.681215Z","shell.execute_reply":"2025-07-17T03:33:36.688408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Collate Function for the DataLoader ---\ndef collate_fn(batch):\n    patches_list = [item['patches'] for item in batch]\n    labels = torch.stack([item['label'] for item in batch])\n    image_ids = [item['image_id'] for item in batch]\n\n    return {\"patches\": patches_list, \"labels\": labels, \"image_ids\": image_ids}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-17T03:33:38.638776Z","iopub.execute_input":"2025-07-17T03:33:38.639822Z","iopub.status.idle":"2025-07-17T03:33:38.644552Z","shell.execute_reply.started":"2025-07-17T03:33:38.639795Z","shell.execute_reply":"2025-07-17T03:33:38.6438Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Main Training Block","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom torch.utils.data import Dataset, DataLoader\n\n# Disable the Decompression Bomb check\nImage.MAX_IMAGE_PIXELS = None\n\n# Hyperparameters\nbatch_size = 4\npatch_size = 224\nnum_patches = 256\ntissue_threshold = 0.9\n\nbase_path = '/kaggle/input/UBC-OCEAN'\ntrain_df = pd.read_csv(os.path.join(base_path, 'train.csv'))\n\n# Create a mapping from string labels to integers\nunique_labels = sorted(train_df['label'].unique())\nlabel_to_int = {label: i for i, label in enumerate(unique_labels)}\nint_to_label = {i: label for label, i in label_to_int.items()}\n\n# testing the code, only take the top 4 reocrds in training file\ntrain_df = train_df.head(20)\n\n# create the dataset using PyTorch Dataset\nubc_dataset = UBCDataset(\n    dataframe=train_df,\n    label_map=label_to_int,\n    patch_size=patch_size,\n    tissue_threshold=tissue_threshold,\n    num_patches=num_patches\n)\n\n# create the training data loader, potentially change shuffle and num_workers\ntrain_loader = DataLoader(\n    ubc_dataset,\n    batch_size=batch_size,\n    shuffle=False,\n    num_workers=0,\n    collate_fn=collate_fn\n)\n\nall_patch_tensors = {}\n\nfor i, batch in enumerate(tqdm(train_loader, desc=\"Processing Batches\")):\n    print(f\"\\n--- Batch {i+1} ---\")\n    \n    patches_list = batch['patches']\n    labels = batch['labels']\n    image_ids = batch['image_ids']\n    \n    print(f\"Number of slides in this batch: {len(patches_list)}\")\n    print(f\"Labels for this batch (as integers): {labels.numpy()}\")\n    print(f\"Image IDs for this batch: {image_ids}\")\n\n    for slide_idx in range(len(image_ids)):\n        slide_id = image_ids[slide_idx]\n        slide_patches = patches_list[slide_idx]\n        slide_label_int = labels[slide_idx].item()\n        \n        print(f\"  - Slide ID: {slide_id}, Label: {int_to_label[slide_label_int]} ({slide_label_int}), Patches: {slide_patches.shape[0]}\")\n        \n        all_patch_tensors[slide_id] = slide_patches\n\nprint(f\"\\n--- Finished processing. Total slides with stored tensors: {len(all_patch_tensors)} ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-17T03:43:00.395215Z","iopub.execute_input":"2025-07-17T03:43:00.39679Z","iopub.status.idle":"2025-07-17T03:59:17.227709Z","shell.execute_reply.started":"2025-07-17T03:43:00.396741Z","shell.execute_reply":"2025-07-17T03:59:17.226755Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Test the data class","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\n# Hyperparameters\nnum_patches = 512\ntissue_threshold = 0.9\n\n# Disable the Decompression Bomb check\nImage.MAX_IMAGE_PIXELS = None\n\nbase_path = '/kaggle/input/UBC-OCEAN'\ntrain_labels_df = pd.read_csv(os.path.join(base_path, 'train.csv'))\n# image_id = str(train_labels_df.loc[2, 'image_id'])\nimage_id = str(4)\nwsi = SlidePatchExtractor(image_id=image_id, tissue_threshold=tissue_threshold, num_patches=num_patches)\nprint(f'The image is {wsi.width} width and {wsi.height} height')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T01:16:03.006967Z","iopub.execute_input":"2025-07-16T01:16:03.007289Z","iopub.status.idle":"2025-07-16T01:16:05.260987Z","shell.execute_reply.started":"2025-07-16T01:16:03.007269Z","shell.execute_reply":"2025-07-16T01:16:05.259681Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize patches for sanity check","metadata":{}},{"cell_type":"code","source":"print(len(wsi.patch_locations))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T01:16:09.566704Z","iopub.execute_input":"2025-07-16T01:16:09.567503Z","iopub.status.idle":"2025-07-16T01:16:09.572643Z","shell.execute_reply.started":"2025-07-16T01:16:09.567475Z","shell.execute_reply":"2025-07-16T01:16:09.571522Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"patch_tensors = wsi.get_all_patch_tensors()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T01:16:14.685377Z","iopub.execute_input":"2025-07-16T01:16:14.68571Z","iopub.status.idle":"2025-07-16T01:16:43.181355Z","shell.execute_reply.started":"2025-07-16T01:16:14.68568Z","shell.execute_reply":"2025-07-16T01:16:43.180421Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(patch_tensors.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T01:16:47.682192Z","iopub.execute_input":"2025-07-16T01:16:47.68251Z","iopub.status.idle":"2025-07-16T01:16:47.687542Z","shell.execute_reply.started":"2025-07-16T01:16:47.682487Z","shell.execute_reply":"2025-07-16T01:16:47.686706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"patch_tensor, patch_image = wsi.get_patch(7)\nprint(patch_tensor)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T00:44:42.791425Z","iopub.execute_input":"2025-07-16T00:44:42.791719Z","iopub.status.idle":"2025-07-16T00:44:43.186458Z","shell.execute_reply.started":"2025-07-16T00:44:42.791701Z","shell.execute_reply":"2025-07-16T00:44:43.185392Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(patch_tensor.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T00:40:43.31565Z","iopub.execute_input":"2025-07-16T00:40:43.316607Z","iopub.status.idle":"2025-07-16T00:40:43.321477Z","shell.execute_reply.started":"2025-07-16T00:40:43.31658Z","shell.execute_reply":"2025-07-16T00:40:43.320232Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nplt.imshow(patch_image)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T00:44:45.462501Z","iopub.execute_input":"2025-07-16T00:44:45.462827Z","iopub.status.idle":"2025-07-16T00:44:45.73628Z","shell.execute_reply.started":"2025-07-16T00:44:45.462798Z","shell.execute_reply":"2025-07-16T00:44:45.735156Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(wsi.patch_locations)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-15T02:23:04.810687Z","iopub.execute_input":"2025-07-15T02:23:04.811426Z","iopub.status.idle":"2025-07-15T02:23:04.815749Z","shell.execute_reply.started":"2025-07-15T02:23:04.811401Z","shell.execute_reply":"2025-07-15T02:23:04.814764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom PIL import Image\n\n# --- 1. Define Thumbnail and Get Image Dimensions ---\n# Get the full resolution image dimensions (width, height)\noriginal_width = wsi.width\noriginal_height = wsi.height\n\npatch_coordinates = wsi.patch_locations\npatch_size = 224\n\n# --- 2. Generate the Thumbnail ---\n# The get_thumbnail function maintains the aspect ratio,\n# creating an image that fits within the given size.\nif wsi.is_tma:\n    thumbnail = wsi.slide.get_thumbnail((1024, 1024))\nelse:\n    thumbnail = Image.open(wsi.thumbnail_path)\n# Get the actual size of the generated thumbnail\nthumb_width, thumb_height = thumbnail.size\n\n# --- 3. Calculate Scaling Factors ---\n# These factors will scale coordinates from the original image to the thumbnail\nwidth_scale = thumb_width / original_width\nheight_scale = thumb_height / original_height\n\n# --- 4. Visualize the Thumbnail and Patches ---\n# Create a figure and axes for plotting\nfig, ax = plt.subplots(figsize=(10, 10))\n\n# Display the thumbnail image\nax.imshow(thumbnail)\n\n# Loop through each patch coordinate to draw it on the thumbnail\nfor x, y in patch_coordinates:\n    # Scale the patch's top-left corner coordinates\n    scaled_x = x * width_scale\n    scaled_y = y * height_scale\n\n    # Scale the patch's dimensions\n    scaled_patch_width = patch_size * width_scale\n    scaled_patch_height = patch_size * height_scale\n\n    # Create a rectangle patch with a red edge and no fill\n    rect = patches.Rectangle(\n        (scaled_x, scaled_y),\n        scaled_patch_width,\n        scaled_patch_height,\n        linewidth=1,\n        edgecolor='r',  # Red color for the patch border\n        facecolor='none'  # No fill\n    )\n\n    # Add the rectangle to the plot\n    ax.add_patch(rect)\n\n# --- 5. Finalize and Show the Plot ---\nax.set_title(\"WSI Thumbnail with Selected Patches\")\nplt.axis('off')  # Hide the axes ticks and labels\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-16T00:44:51.750025Z","iopub.execute_input":"2025-07-16T00:44:51.750372Z","iopub.status.idle":"2025-07-16T00:44:52.762217Z","shell.execute_reply.started":"2025-07-16T00:44:51.750349Z","shell.execute_reply":"2025-07-16T00:44:52.760478Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"hello\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}