{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.9","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":8089,"databundleVersionId":44321,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":19989,"databundleVersionId":1160143,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":10342768,"sourceType":"datasetVersion","datasetId":6404701}],"dockerImageVersionId":30056,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div align='center'><font size=\"5\" color='#353B47'>Image Segmentation</font></div>\n<div align='center'><font size=\"4\" color=\"#353B47\">Using U-Net with Keras</font></div>\n<br>\n<hr>","metadata":{}},{"cell_type":"code","source":"pip install tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-08T10:42:09.200885Z","iopub.execute_input":"2025-04-08T10:42:09.201230Z","iopub.status.idle":"2025-04-08T10:42:15.556324Z","shell.execute_reply.started":"2025-04-08T10:42:09.201155Z","shell.execute_reply":"2025-04-08T10:42:15.555410Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ndef process_csv(file_path, base_path):\n    \"\"\"\n    Đọc file CSV, tách từng dòng dựa vào khoảng trắng và lấy các cột cần thiết.\n    Thêm chuỗi `base_path` vào cột `image_path`, chuyển label '3' thành '1',\n    và giữ lại các cột bounding box (cột 3-6 cho box 1, cột 7-10 cho box 2).\n    Nếu thiếu cột, gán giá trị bounding box = 0.\n\n    Args:\n        file_path (str): Đường dẫn tới file CSV.\n        base_path (str): Chuỗi cần thêm vào đầu cột `image_path`.\n\n    Returns:\n        pandas.DataFrame: DataFrame chứa các cột `image_path`, `label`, và bounding box.\n    \"\"\"\n    result = []\n\n    with open(file_path, 'r') as file:\n        for line in file:\n            # Tách dòng thành các phần tử bằng khoảng trắng\n            splitted = line.strip().split()\n\n            # Đảm bảo có ít nhất 2 cột (image_path và label)\n            if len(splitted) >= 2:\n                label = splitted[1]\n                \n                # Nếu label là '3', chuyển thành '1'\n                if label == '3':\n                    label = '1'\n                \n                # Tạo dictionary với image_path, label và bounding box\n                row = {\n                    'image_path': base_path + splitted[0],\n                    'label': label,\n                    'bbox1_x': splitted[2] if len(splitted) > 2 else '0',  # Cột 3\n                    'bbox1_y': splitted[3] if len(splitted) > 3 else '0',  # Cột 4\n                    'bbox1_w': splitted[4] if len(splitted) > 4 else '0',  # Cột 5\n                    'bbox1_h': splitted[5] if len(splitted) > 5 else '0',  # Cột 6\n                    'bbox2_x': splitted[6] if len(splitted) > 6 else '0',  # Cột 7\n                    'bbox2_y': splitted[7] if len(splitted) > 7 else '0',  # Cột 8\n                    'bbox2_w': splitted[8] if len(splitted) > 8 else '0',  # Cột 9\n                    'bbox2_h': splitted[9] if len(splitted) > 9 else '0'   # Cột 10\n                }\n                result.append(row)\n\n    # Chuyển danh sách sang DataFrame\n    data = pd.DataFrame(result)\n    return data\n\n# Gọi hàm để xử lý dữ liệu\ndata_train = process_csv(\"/kaggle/input/databee/train/train/gt_one.csv\", \"/kaggle/input/databee/train/train/\")\ndata_test = process_csv(\"/kaggle/input/databee/test/test/gt_one.csv\", \"/kaggle/input/databee/test/test/\")\ndata_val = process_csv(\"/kaggle/input/databee/val/val/gt_one.csv\", \"/kaggle/input/databee/val/val/\")\n\n# Hiển thị kết quả\nprint(\"Số lượng dòng trong data_train:\")\nprint(data_train.count())\nprint(\"\\nSố lượng dòng trong data_test:\")\nprint(data_test.count())\nprint(\"\\n5 dòng đầu tiên của data_train:\")\nprint(data_train.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-08T10:42:18.118264Z","iopub.execute_input":"2025-04-08T10:42:18.118556Z","iopub.status.idle":"2025-04-08T10:42:18.241361Z","shell.execute_reply.started":"2025-04-08T10:42:18.118530Z","shell.execute_reply":"2025-04-08T10:42:18.240576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport cv2\nimport os\nfrom pathlib import Path\nimport zipfile\nfrom tqdm import tqdm\n\n# Hàm tạo mặt nạ từ bounding box\ndef create_mask_from_bbox(image_path, bbox1_x, bbox1_y, bbox1_w, bbox1_h, \n                         bbox2_x, bbox2_y, bbox2_w, bbox2_h):\n    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)\n    if img is None:\n        raise ValueError(f\"Không thể đọc ảnh từ {image_path}\")\n    \n    orig_height, orig_width = img.shape[:2]\n    mask = np.zeros((orig_height, orig_width), dtype=np.uint8)\n    \n    try:\n        bbox1_x1, bbox1_y1 = int(float(bbox1_x)), int(float(bbox1_y))\n        bbox1_x2, bbox1_y2 = int(float(bbox1_w)), int(float(bbox1_h))\n        bbox2_x1, bbox2_y1 = int(float(bbox2_x)), int(float(bbox2_y))\n        bbox2_x2, bbox2_y2 = int(float(bbox2_w)), int(float(bbox2_h))\n    except ValueError:\n        return mask\n    \n    for (x1, y1, x2, y2) in [(bbox1_x1, bbox1_y1, bbox1_x2, bbox1_y2), \n                            (bbox2_x1, bbox2_y1, bbox2_x2, bbox2_y2)]:\n        if x1 != 0 or y1 != 0 or x2 != 0 or y2 != 0:\n            x1, x2 = min(x1, x2), max(x1, x2)\n            y1, y2 = min(y1, y2), max(y1, y2)\n            x1, y1 = max(0, x1), max(0, y1)\n            x2, y2 = min(orig_width, x2), min(orig_height, y2)\n            if x1 < x2 and y1 < y2:\n                mask[y1:y2, x1:x2] = 1\n    \n    return mask\n\n# Hàm tạo mặt nạ cho dataset và lưu vào thư mục\ndef generate_masks_for_dataset(dataframe, output_dir):\n    Path(output_dir).mkdir(parents=True, exist_ok=True)\n    records = []\n    \n    for idx, row in tqdm(dataframe.iterrows(), total=len(dataframe), desc=\"Tạo mask\"):\n        try:\n            mask = create_mask_from_bbox(\n                row['image_path'], row['bbox1_x'], row['bbox1_y'], row['bbox1_w'], row['bbox1_h'],\n                row['bbox2_x'], row['bbox2_y'], row['bbox2_w'], row['bbox2_h']\n            )\n            # Lấy tên file gốc từ image_path (không bao gồm phần mở rộng)\n            image_name = os.path.basename(row['image_path']).rsplit('.', 1)[0]\n            mask_path = os.path.join(output_dir, f\"{image_name}_mask.png\")\n            cv2.imwrite(mask_path, mask * 255)\n            records.append({\n                'image_path': row['image_path'],\n                'mask_path': mask_path,\n                'label': row['label']\n            })\n        except Exception:\n            pass  # Bỏ qua lỗi, không hiển thị thông báo\n    \n    return pd.DataFrame(records)\n\n# Hàm lưu DataFrame vào CSV và nén thành file zip\ndef save_to_csv_and_zip(df, csv_path, zip_path, mask_dir):\n    df.to_csv(csv_path, index=False)\n    with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:\n        zipf.write(csv_path, os.path.basename(csv_path))\n        for mask_file in Path(mask_dir).glob(\"*.png\"):\n            zipf.write(mask_file, os.path.join(\"masks\", mask_file.name))\n\n# Giả định rằng data_train, data_val, data_test đã được định nghĩa trước đó\n# Nếu chưa, bạn cần load chúng từ file CSV gốc, ví dụ:\n# data_train = pd.read_csv('path_to_train.csv')\n# data_val = pd.read_csv('path_to_val.csv')\n# data_test = pd.read_csv('path_to_test.csv')\n\n# Chạy cho tập train\noutput_dir_train = \"/kaggle/working/masks/train/\"\ntrain_info_df = generate_masks_for_dataset(data_train, output_dir_train)\ntrain_csv_path = \"/kaggle/working/train_info.csv\"\ntrain_zip_path = \"/kaggle/working/train_info.zip\"\nsave_to_csv_and_zip(train_info_df, train_csv_path, train_zip_path, output_dir_train)\n\n# Chạy cho tập validation\noutput_dir_val = \"/kaggle/working/masks/val/\"  # Sửa từ output_dir_train thành output_dir_val\nval_info_df = generate_masks_for_dataset(data_val, output_dir_val)  # Sửa từ train_info_df thành val_info_df\nval_csv_path = \"/kaggle/working/val_info.csv\"  # Sửa từ train_csv_path thành val_csv_path\nval_zip_path = \"/kaggle/working/val_info.zip\"  # Sửa từ train_zip_path thành val_zip_path\nsave_to_csv_and_zip(val_info_df, val_csv_path, val_zip_path, output_dir_val)\n\n# Chạy cho tập test\noutput_dir_test = \"/kaggle/working/masks/test/\"\ntest_info_df = generate_masks_for_dataset(data_test, output_dir_test)\ntest_csv_path = \"/kaggle/working/test_info.csv\"\ntest_zip_path = \"/kaggle/working/test_info.zip\"\nsave_to_csv_and_zip(test_info_df, test_csv_path, test_zip_path, output_dir_test)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-08T10:45:18.644361Z","iopub.execute_input":"2025-04-08T10:45:18.644663Z","iopub.status.idle":"2025-04-08T10:46:44.983929Z","shell.execute_reply.started":"2025-04-08T10:45:18.644637Z","shell.execute_reply":"2025-04-08T10:46:44.983140Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import load_img, img_to_array\n\n# Đọc file CSV cho 3 tập: train, validation, test\ntrain_data = pd.read_csv('/kaggle/working/train_info.csv')\nval_data = pd.read_csv('/kaggle/working/val_info.csv')\ntest_data = pd.read_csv('/kaggle/working/test_info.csv')\n\n# Hàm tiền xử lý ảnh và mặt nạ\ndef preprocess_image(image_path, mask_path, target_size=(256, 256)):\n    try:\n        # Đọc ảnh\n        image = load_img(image_path, target_size=target_size)\n        image = img_to_array(image) / 255.0  # Chuẩn hóa về [0, 1]\n        \n        # Đọc mặt nạ (giả sử mặt nạ là ảnh grayscale)\n        mask = load_img(mask_path, target_size=target_size, color_mode=\"grayscale\")\n        mask = img_to_array(mask) / 255.0  # Chuẩn hóa về [0, 1]\n        mask = np.round(mask)  # Đảm bảo giá trị nhị phân (0 hoặc 1)\n        \n        return image, mask\n    except Exception as e:\n        print(f\"Error loading {image_path} or {mask_path}: {e}\")\n        return None, None\n\n# Hàm generator để load dữ liệu theo batch\ndef data_generator(dataframe, target_size=(256, 256), batch_size=16):\n    while True:\n        for start in range(0, len(dataframe), batch_size):\n            end = min(start + batch_size, len(dataframe))\n            batch_images = []\n            batch_masks = []\n            for idx in range(start, end):\n                image, mask = preprocess_image(dataframe['image_path'][idx], \n                                              dataframe['mask_path'][idx], \n                                              target_size)\n                if image is not None and mask is not None:\n                    batch_images.append(image)\n                    batch_masks.append(mask)\n            if batch_images:\n                yield np.array(batch_images), np.array(batch_masks)\n\n# Tạo dataset từ generator cho 3 tập\nbatch_size = 16\ntrain_dataset = tf.data.Dataset.from_generator(\n    lambda: data_generator(train_data, batch_size=batch_size),\n    output_types=(tf.float32, tf.float32),\n    output_shapes=([None, 256, 256, 3], [None, 256, 256, 1])\n).prefetch(tf.data.AUTOTUNE)\n\nval_dataset = tf.data.Dataset.from_generator(\n    lambda: data_generator(val_data, batch_size=batch_size),\n    output_types=(tf.float32, tf.float32),\n    output_shapes=([None, 256, 256, 3], [None, 256, 256, 1])\n).prefetch(tf.data.AUTOTUNE)\n\ntest_dataset = tf.data.Dataset.from_generator(\n    lambda: data_generator(test_data, batch_size=batch_size),\n    output_types=(tf.float32, tf.float32),\n    output_shapes=([None, 256, 256, 3], [None, 256, 256, 1])\n).prefetch(tf.data.AUTOTUNE)\n\n# In số lượng mẫu trong mỗi tập để kiểm tra\nprint(\"Number of samples in Train set:\", len(train_data))\nprint(\"Number of samples in Validation set:\", len(val_data))\nprint(\"Number of samples in Test set:\", len(test_data))\n\n# Lấy một batch từ mỗi dataset để kiểm tra shape\ntrain_iter = iter(train_dataset)\nval_iter = iter(val_dataset)\ntest_iter = iter(test_dataset)\n\ntrain_images, train_masks = next(train_iter)\nval_images, val_masks = next(val_iter)\ntest_images, test_masks = next(test_iter)\n\nprint(\"Train batch shape (images, masks):\", train_images.shape, train_masks.shape)\nprint(\"Validation batch shape (images, masks):\", val_images.shape, val_masks.shape)\nprint(\"Test batch shape (images, masks):\", test_images.shape, test_masks.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-08T10:47:21.385582Z","iopub.execute_input":"2025-04-08T10:47:21.385864Z","iopub.status.idle":"2025-04-08T10:47:29.062380Z","shell.execute_reply.started":"2025-04-08T10:47:21.385840Z","shell.execute_reply":"2025-04-08T10:47:29.061168Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Hàm hiển thị hình ảnh và mặt nạ\ndef display_image_mask(image, mask, title=\"Image and Mask\"):\n    # Chuyển EagerTensor thành NumPy array\n    image = image.numpy() if isinstance(image, tf.Tensor) else image\n    mask = mask.numpy() if isinstance(mask, tf.Tensor) else mask\n    \n    plt.figure(figsize=(10, 5))\n    \n    # Hiển thị hình ảnh gốc\n    plt.subplot(1, 2, 1)\n    plt.title(\"Image\")\n    plt.imshow(image)\n    plt.axis('off')\n    \n    # Hiển thị mặt nạ\n    plt.subplot(1, 2, 2)\n    plt.title(\"Mask\")\n    plt.imshow(mask.squeeze(), cmap='gray')  # squeeze() để loại bỏ chiều kênh (1) của mặt nạ\n    plt.axis('off')\n    \n    plt.suptitle(title)\n    plt.show()\n\n# Tạo iterator để lấy dữ liệu từ các dataset\ntrain_iter = iter(train_dataset)\nval_iter = iter(val_dataset)\ntest_iter = iter(test_dataset)\n\n# Lấy một batch từ mỗi dataset\ntrain_images, train_masks = next(train_iter)\nval_images, val_masks = next(val_iter)\ntest_images, test_masks = next(test_iter)\n\n# Hiển thị 3 cặp từ tập train\nprint(\"Displaying 3 samples from the training set:\")\nfor i in range(min(3, len(train_images))):  # Đảm bảo không vượt quá số mẫu trong batch\n    display_image_mask(train_images[i], train_masks[i], title=f\"Train Sample {i+1}\")\n\n# Hiển thị 3 cặp từ tập validation\nprint(\"Displaying 3 samples from the validation set:\")\nfor i in range(min(3, len(val_images))):  # Đảm bảo không vượt quá số mẫu trong batch\n    display_image_mask(val_images[i], val_masks[i], title=f\"Validation Sample {i+1}\")\n\n# Hiển thị 3 cặp từ tập test\nprint(\"Displaying 3 samples from the test set:\")\nfor i in range(min(3, len(test_images))):  # Đảm bảo không vượt quá số mẫu trong batch\n    display_image_mask(test_images[i], test_masks[i], title=f\"Test Sample {i+1}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-08T10:49:04.625998Z","iopub.execute_input":"2025-04-08T10:49:04.626285Z","iopub.status.idle":"2025-04-08T10:49:06.211076Z","shell.execute_reply.started":"2025-04-08T10:49:04.626261Z","shell.execute_reply":"2025-04-08T10:49:06.210358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Import các thư viện cần thiết\nfrom tensorflow.keras.layers import Input, Conv2D, MaxPooling2D, UpSampling2D, concatenate\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, Callback  # Thêm Callback và ModelCheckpoint\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\n\n# Định nghĩa các độ đo tùy chỉnh: IoU và Dice Coefficient\ndef iou_metric(y_true, y_pred):\n    y_pred = tf.cast(y_pred > 0.5, tf.float32)\n    y_true = tf.cast(y_true, tf.float32)\n    intersection = tf.reduce_sum(y_true * y_pred)\n    union = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) - intersection\n    return intersection / (union + tf.keras.backend.epsilon())\n\ndef dice_metric(y_true, y_pred):\n    y_pred = tf.cast(y_pred > 0.5, tf.float32)\n    y_true = tf.cast(y_true, tf.float32)\n    intersection = tf.reduce_sum(y_true * y_pred)\n    return (2. * intersection) / (tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) + tf.keras.backend.epsilon())\n\n# Xây dựng mô hình U-Net\ndef unet_model(input_size=(256, 256, 3)):\n    inputs = Input(input_size)\n    c1 = Conv2D(64, (3, 3), activation='relu', padding='same')(inputs)\n    c1 = Conv2D(64, (3, 3), activation='relu', padding='same')(c1)\n    p1 = MaxPooling2D((2, 2))(c1)\n    \n    c2 = Conv2D(128, (3, 3), activation='relu', padding='same')(p1)\n    c2 = Conv2D(128, (3, 3), activation='relu', padding='same')(c2)\n    p2 = MaxPooling2D((2, 2))(c2)\n    \n    c3 = Conv2D(256, (3, 3), activation='relu', padding='same')(p2)\n    c3 = Conv2D(256, (3, 3), activation='relu', padding='same')(c3)\n    \n    u4 = UpSampling2D((2, 2))(c3)\n    u4 = concatenate([u4, c2])\n    c4 = Conv2D(128, (3, 3), activation='relu', padding='same')(u4)\n    c4 = Conv2D(128, (3, 3), activation='relu', padding='same')(c4)\n    \n    u5 = UpSampling2D((2, 2))(c4)\n    u5 = concatenate([u5, c1])\n    c5 = Conv2D(64, (3, 3), activation='relu', padding='same')(u5)\n    c5 = Conv2D(64, (3, 3), activation='relu', padding='same')(c5)\n    \n    outputs = Conv2D(1, (1, 1), activation='sigmoid')(c5)\n    model = Model(inputs, outputs)\n    return model\n\n# Callback để hiển thị thanh tiến trình\nclass TQDMProgressBar(Callback):\n    def on_train_begin(self, logs=None):\n        self.epochs = self.params['epochs']\n        self.progress_bar = tqdm(total=self.epochs, desc='Training Progress', unit='epoch')\n\n    def on_epoch_end(self, epoch, logs=None):\n        self.progress_bar.update(1)\n        self.progress_bar.set_postfix({\n            'loss': f\"{logs.get('loss'):.4f}\",\n            'val_loss': f\"{logs.get('val_loss'):.4f}\",\n            'iou': f\"{logs.get('iou_metric'):.4f}\",\n            'val_iou': f\"{logs.get('val_iou_metric'):.4f}\",\n            'dice': f\"{logs.get('dice_metric'):.4f}\",\n            'val_dice': f\"{logs.get('val_dice_metric'):.4f}\"\n        })\n\n    def on_train_end(self, logs=None):\n        self.progress_bar.close()\n\n# Khởi tạo và biên dịch mô hình\nmodel = unet_model()\nmodel.compile(optimizer='adam', \n              loss='binary_crossentropy', \n              metrics=[iou_metric, dice_metric])\n\n# Thêm Early Stopping callback\nearly_stopping = EarlyStopping(\n    monitor='val_iou_metric',  # Theo dõi validation IoU\n    mode='max',                # Tối đa hóa IoU\n    patience=25,               # Dừng sau 10 epoch nếu không cải thiện\n    restore_best_weights=True, # Khôi phục trọng số tốt nhất\n    verbose=1\n)\n\n# Thêm ModelCheckpoint callback để lưu mô hình tốt nhất\ncheckpoint = ModelCheckpoint(\n    'best_unet_model.h5',      # Đường dẫn lưu mô hình tốt nhất\n    monitor='val_iou_metric',  # Theo dõi validation IoU\n    mode='max',                # Tối đa hóa IoU\n    save_best_only=True,       # Chỉ lưu khi mô hình cải thiện\n    verbose=1                  # Hiển thị thông báo khi lưu\n)\n\n# Huấn luyện mô hình với cả TQDM, Early Stopping và ModelCheckpoint\nhistory = model.fit(\n    train_dataset,\n    validation_data=val_dataset,\n    steps_per_epoch=len(train_data) // batch_size,\n    validation_steps=len(val_data) // batch_size,\n    epochs=150,\n    callbacks=[TQDMProgressBar(), early_stopping, checkpoint],  # Thêm checkpoint vào callbacks\n    verbose=0\n)\n\n# Lưu mô hình cuối cùng (tùy chọn)\nmodel.save('unet_model.h5')\n\n# Đánh giá mô hình\ntest_loss, test_iou, test_dice = model.evaluate(\n    test_dataset,\n    steps=len(test_data) // batch_size,\n    verbose=1\n)\nprint(f\"Test Loss: {test_loss:.4f}\")\nprint(f\"Test IoU: {test_iou:.4f}\")\nprint(f\"Test Dice Coefficient: {test_dice:.4f}\")\n\n# Vẽ biểu đồ\nplt.figure(figsize=(18, 4))\nplt.subplot(1, 3, 1)\nplt.plot(history.history['loss'], label='Train Loss')\nplt.plot(history.history['val_loss'], label='Validation Loss')\nplt.title('Loss over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\n\nplt.subplot(1, 3, 2)\nplt.plot(history.history['iou_metric'], label='Train IoU')\nplt.plot(history.history['val_iou_metric'], label='Validation IoU')\nplt.title('IoU over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('IoU')\nplt.legend()\n\nplt.subplot(1, 3, 3)\nplt.plot(history.history['dice_metric'], label='Train Dice')\nplt.plot(history.history['val_dice_metric'], label='Validation Dice')\nplt.title('Dice Coefficient over Epochs')\nplt.xlabel('Epoch')\nplt.ylabel('Dice')\nplt.legend()\n\nplt.tight_layout()\nplt.show()\n\n# Hiển thị dự đoán\ntest_iterator = iter(test_dataset)\ntest_images, test_masks = next(test_iterator)\npredictions = model.predict(test_images)\n\nfor i in range(min(3, len(test_images))):\n    plt.figure(figsize=(15, 5))\n    \n    test_image = test_images[i].numpy() if isinstance(test_images[i], tf.Tensor) else test_images[i]\n    test_mask = test_masks[i].numpy() if isinstance(test_masks[i], tf.Tensor) else test_masks[i]\n    prediction = predictions[i]\n    \n    plt.subplot(1, 3, 1)\n    plt.title(\"Image\")\n    plt.imshow(test_image)\n    plt.axis('off')\n    \n    plt.subplot(1, 3, 2)\n    plt.title(\"True Mask\")\n    plt.imshow(test_mask.squeeze(), cmap='gray')\n    plt.axis('off')\n    \n    plt.subplot(1, 3, 3)\n    plt.title(\"Predicted Mask\")\n    plt.imshow(prediction.squeeze(), cmap='gray')\n    plt.axis('off')\n    \n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-08T10:56:24.386355Z","iopub.execute_input":"2025-04-08T10:56:24.386676Z","iopub.status.idle":"2025-04-08T11:44:18.340551Z","shell.execute_reply.started":"2025-04-08T10:56:24.386645Z","shell.execute_reply":"2025-04-08T11:44:18.339719Z"}},"outputs":[],"execution_count":null}]}