From 6b0ba9d9a0d9a20f40780fda8458cff3151a3892 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Wed, 23 Oct 2024 16:41:56 +1000 Subject: [PATCH 01/26] Create 3DUNET 48790835 --- recognition/3DUNET 48790835 | 1 + 1 file changed, 1 insertion(+) create mode 100644 recognition/3DUNET 48790835 diff --git a/recognition/3DUNET 48790835 b/recognition/3DUNET 48790835 new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/recognition/3DUNET 48790835 @@ -0,0 +1 @@ + From 6df9c1aa782ae623bdd69346e112cf802a08cd79 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Wed, 23 Oct 2024 16:44:43 +1000 Subject: [PATCH 02/26] Create README.md --- recognition/3D-UNT 48790835/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 recognition/3D-UNT 48790835/README.md diff --git a/recognition/3D-UNT 48790835/README.md b/recognition/3D-UNT 48790835/README.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/recognition/3D-UNT 48790835/README.md @@ -0,0 +1 @@ + From 54be22444d360089d2e2b241f7ad5f1374fdfedb Mon Sep 17 00:00:00 2001 From: Han1zen Date: Wed, 23 Oct 2024 16:48:23 +1000 Subject: [PATCH 03/26] Add files via upload --- recognition/3D-UNT 48790835/dataset.py | 145 +++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 recognition/3D-UNT 48790835/dataset.py diff --git a/recognition/3D-UNT 48790835/dataset.py b/recognition/3D-UNT 48790835/dataset.py new file mode 100644 index 000000000..0cce4e84d --- /dev/null +++ b/recognition/3D-UNT 48790835/dataset.py @@ -0,0 +1,145 @@ +import os +import numpy as np +import nibabel as nib +from tqdm import tqdm +import torch +from torch.utils.data import Dataset +from scipy.ndimage import zoom + +def to_channels(arr: np.ndarray, dtype=np.uint8) -> np.ndarray: + """ + 将三维数组转换为带有通道的独热编码四维数组。 + """ + channels = np.unique(arr) + res = np.zeros(arr.shape + (len(channels),), dtype=dtype) + for idx, c in enumerate(channels): + res[..., idx][arr == c] = 1 + return res + +def applyOrientation(niftiImage, interpolation='linear', scale=1): + """ + 应用方向和缩放到 NIfTI 图像。 + + 参数: + - niftiImage:nibabel NIfTI 图像对象。 + - interpolation:插值方法,'linear' 或 'nearest'。 + - scale:缩放因子。 + """ + # 这里进行重采样或重新取样等操作 + # 由于具体实现取决于您的需求,以下是一个示例(需要根据实际情况修改): + data = niftiImage.get_fdata() + affine = niftiImage.affine + + # 进行缩放(如果需要) + if scale != 1: + zoom_factors = [scale, scale, scale] + data = zoom(data, zoom=zoom_factors, order=1 if interpolation == 'linear' else 0) + + # 创建新的 NIfTI 图像 + new_nifti = nib.Nifti1Image(data, affine) + return new_nifti + +class MedicalDataset3D(Dataset): + """ + 自定义数据集类,用于加载 3D 医学图像。 + """ + def __init__(self, image_paths, label_paths=None, transform=None, normImage=False, categorical=False, dtype=np.float32, orient=False): + """ + 参数: + - image_paths:图像文件路径列表。 + - label_paths:标签文件路径列表(如果有)。 + - transform:数据增强变换。 + - normImage:是否标准化图像。 + - categorical:是否将标签转换为独热编码。 + - dtype:数据类型。 + - orient:是否应用方向和缩放。 + """ + self.image_paths = image_paths + self.label_paths = label_paths + self.transform = transform + self.normImage = normImage + self.categorical = categorical + self.dtype = dtype + self.orient = orient + + def __len__(self): + return len(self.image_paths) + + def __getitem__(self, idx): + # 加载图像 + image_nifti = nib.load(self.image_paths[idx]) + if self.orient: + image_nifti = applyOrientation(image_nifti, interpolation='linear', scale=1) + image = image_nifti.get_fdata(caching='unchanged') + if len(image.shape) == 4: + image = image[:, :, :, 0] + image = image.astype(self.dtype) + if self.normImage: + image = (image - image.mean()) / image.std() + image = np.expand_dims(image, axis=0) # 添加通道维度 + + # 如果有标签,加载标签 + if self.label_paths: + label_nifti = nib.load(self.label_paths[idx]) + if self.orient: + label_nifti = applyOrientation(label_nifti, interpolation='nearest', scale=1) + label = label_nifti.get_fdata(caching='unchanged') + if len(label.shape) == 4: + label = label[:, :, :, 0] + label = label.astype(self.dtype) + if self.categorical: + label = to_channels(label, dtype=self.dtype) + label = np.moveaxis(label, -1, 0) # 将通道维度移到前面 + else: + label = np.expand_dims(label, axis=0) # 添加通道维度 + else: + label = None + + # 应用数据增强(如果有) + if self.transform: + # 注意,您需要确保 transform 适用于 3D 图像数据 + if label is not None: + data = {"image": image, "mask": label} + augmented = self.transform(**data) + image = augmented["image"] + label = augmented["mask"] + else: + data = {"image": image} + augmented = self.transform(**data) + image = augmented["image"] + + # 转换为张量 + image = torch.tensor(image, dtype=torch.float32) + if label is not None: + label = torch.tensor(label, dtype=torch.float32) + return image, label + else: + return image + +def load_image_paths(data_dir, split='train'): + """ + 获取图像和标签的文件路径列表。 + + 参数: + - data_dir:数据集目录。 + - split:数据集划分,'train'、'val' 或 'test'。 + + 返回: + - image_paths:图像文件路径列表。 + - label_paths:标签文件路径列表。 + """ + # 根据您的数据组织方式,实现获取文件路径的逻辑 + # dataset.py + + +def load_image_paths(image_dir, label_dir): + image_paths = sorted([ + os.path.join(image_dir, f) for f in os.listdir(image_dir) + if f.endswith('.nii') or f.endswith('.nii.gz') + ]) + label_paths = sorted([ + os.path.join(label_dir, f) for f in os.listdir(label_dir) + if f.endswith('.nii') or f.endswith('.nii.gz') + ]) + return image_paths, label_paths + From 1b297dceb3ba0a3665a54c157653e96e1403d65e Mon Sep 17 00:00:00 2001 From: Han1zen Date: Wed, 23 Oct 2024 16:59:46 +1000 Subject: [PATCH 04/26] Add files via upload --- recognition/3D-UNT 48790835/modules.py | 71 +++++++++++++++++++ recognition/3D-UNT 48790835/predict.py | 76 ++++++++++++++++++++ recognition/3D-UNT 48790835/train.py | 96 ++++++++++++++++++++++++++ recognition/3D-UNT 48790835/utils.py | 40 +++++++++++ 4 files changed, 283 insertions(+) create mode 100644 recognition/3D-UNT 48790835/modules.py create mode 100644 recognition/3D-UNT 48790835/predict.py create mode 100644 recognition/3D-UNT 48790835/train.py create mode 100644 recognition/3D-UNT 48790835/utils.py diff --git a/recognition/3D-UNT 48790835/modules.py b/recognition/3D-UNT 48790835/modules.py new file mode 100644 index 000000000..0c70d2b15 --- /dev/null +++ b/recognition/3D-UNT 48790835/modules.py @@ -0,0 +1,71 @@ + +import torch +import torch.nn as nn +import torch.nn.functional as F + +class DoubleConv(nn.Module): + """ + 两次卷积操作:Conv3D => BN => ReLU => Conv3D => BN => ReLU + """ + def __init__(self, in_channels, out_channels): + super(DoubleConv, self).__init__() + self.double_conv = nn.Sequential( + nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1), + nn.BatchNorm3d(out_channels), + nn.ReLU(inplace=True), + nn.Conv3d(out_channels, out_channels, kernel_size=3, padding=1), + nn.BatchNorm3d(out_channels), + nn.ReLU(inplace=True) + ) + + def forward(self, x): + return self.double_conv(x) + +class UNet3D(nn.Module): + def __init__(self, in_channels=1, out_channels=1, features=[32, 64, 128, 256]): + super(UNet3D, self).__init__() + self.ups = nn.ModuleList() + self.downs = nn.ModuleList() + self.pool = nn.MaxPool3d(kernel_size=2, stride=2) + + # 下采样部分 + for feature in features: + self.downs.append(DoubleConv(in_channels, feature)) + in_channels = feature + + # 上采样部分 + for feature in reversed(features): + self.ups.append( + nn.ConvTranspose3d(feature * 2, feature, kernel_size=2, stride=2) + ) + self.ups.append(DoubleConv(feature * 2, feature)) + + self.bottleneck = DoubleConv(features[-1], features[-1] * 2) + self.final_conv = nn.Conv3d(features[0], out_channels, kernel_size=1) + + def forward(self, x): + skip_connections = [] + + # 下采样路径 + for down in self.downs: + x = down(x) + skip_connections.append(x) + x = self.pool(x) + + x = self.bottleneck(x) + skip_connections = skip_connections[::-1] + + # 上采样路径 + for idx in range(0, len(self.ups), 2): + x = self.ups[idx](x) + skip_connection = skip_connections[idx // 2] + + # 如果尺寸不匹配,进行裁剪(可根据需要调整) + if x.shape != skip_connection.shape: + x = F.interpolate(x, size=skip_connection.shape[2:]) + + concat_skip = torch.cat((skip_connection, x), dim=1) + x = self.ups[idx + 1](concat_skip) + + return self.final_conv(x) + diff --git a/recognition/3D-UNT 48790835/predict.py b/recognition/3D-UNT 48790835/predict.py new file mode 100644 index 000000000..cd4738f26 --- /dev/null +++ b/recognition/3D-UNT 48790835/predict.py @@ -0,0 +1,76 @@ +import torch +from torch.utils.data import DataLoader +from modules import UNet3D +from dataset import MedicalDataset3D, load_image_paths +import matplotlib.pyplot as plt +import numpy as np + +DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' + +# 加载测试数据集 +# predict.py + +from dataset import MedicalDataset3D, load_image_paths + +# 设置数据路径 +data_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/data' + + +# 获取测试集的文件路径 +test_image_paths, test_label_paths = load_image_paths(data_dir, split='test') + +# 创建测试数据集 +test_dataset = MedicalDataset3D( + image_paths=test_image_paths, + label_paths=test_label_paths, + normImage=True, + categorical=False, + dtype=np.float32, + orient=True # 如果需要应用方向和缩放 +) + +# 创建数据加载器 +test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False) + +# 初始化模型并加载训练好的权重 +model = UNet3D(in_channels=1, out_channels=1).to(DEVICE) +model.load_state_dict(torch.load('checkpoint_epoch_50.pth', map_location=DEVICE)) +model.eval() + +def dice_coefficient(preds, targets, smooth=1e-6): + preds = (preds > 0.5).float() + intersection = (preds * targets).sum() + union = preds.sum() + targets.sum() + dice = (2 * intersection + smooth) / (union + smooth) + return dice.item() + +dice_scores = [] + +with torch.no_grad(): + for data, targets in test_loader: + data = data.to(DEVICE, dtype=torch.float) + targets = targets.to(DEVICE, dtype=torch.float) + + predictions = model(data) + dice = dice_coefficient(torch.sigmoid(predictions), targets) + dice_scores.append(dice) + + # 可视化结果(仅展示中间的一张切片) + slice_idx = data.shape[2] // 2 + input_slice = data.cpu().numpy()[0, 0, slice_idx, :, :] + target_slice = targets.cpu().numpy()[0, 0, slice_idx, :, :] + pred_slice = torch.sigmoid(predictions).cpu().numpy()[0, 0, slice_idx, :, :] + + fig, axes = plt.subplots(1, 3, figsize=(12, 4)) + axes[0].imshow(input_slice, cmap='gray') + axes[0].set_title('Input') + axes[1].imshow(target_slice, cmap='gray') + axes[1].set_title('Ground Truth') + axes[2].imshow(pred_slice, cmap='gray') + axes[2].set_title('Prediction') + plt.show() + +# 打印平均 Dice 系数 +avg_dice = np.mean(dice_scores) +print(f'Average Dice Similarity Coefficient on Test Set: {avg_dice:.4f}') + diff --git a/recognition/3D-UNT 48790835/train.py b/recognition/3D-UNT 48790835/train.py new file mode 100644 index 000000000..65188e2b9 --- /dev/null +++ b/recognition/3D-UNT 48790835/train.py @@ -0,0 +1,96 @@ +import os +import torch +from torch.utils.data import DataLoader +from sklearn.model_selection import train_test_split +from dataset import MedicalDataset3D, load_image_paths +from modules import UNet3D +from utils import DiceLoss, calculate_dice_coefficient + +# 设置超参数 +DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' +LEARNING_RATE = 1e-4 +BATCH_SIZE = 1 # 根据您的显存大小调整批量大小 +EPOCHS = 50 + +# 指定图像和标签目录 +image_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/' \ + 'Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/' \ + 'data/HipMRI_study_complete_release_v1/semantic_MRs_anon' + +label_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/' \ + 'Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/' \ + 'data/HipMRI_study_complete_release_v1/semantic_labels_anon' + + +# 加载图像和标签路径 +image_paths, label_paths = load_image_paths(image_dir, label_dir) + +# 将数据集划分为训练集和验证集 +train_images, val_images, train_labels, val_labels = train_test_split( + image_paths, label_paths, test_size=0.2, random_state=42) + +# 创建数据集和数据加载器 +train_dataset = MedicalDataset3D(train_images, train_labels) +val_dataset = MedicalDataset3D(val_images, val_labels) + +train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True) +val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False) + +# 初始化模型、损失函数和优化器 +model = UNet3D(in_channels=1, out_channels=1).to(DEVICE) +criterion = DiceLoss() +optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE) + +# 定义训练函数 +def train(): + for epoch in range(EPOCHS): + print(f"Epoch {epoch+1}/{EPOCHS}") + model.train() + train_loss = 0.0 + for batch_idx, (images, labels) in enumerate(train_loader): + images = images.to(DEVICE, dtype=torch.float) + labels = labels.to(DEVICE, dtype=torch.float) + + optimizer.zero_grad() + outputs = model(images) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + + train_loss += loss.item() + + if (batch_idx + 1) % 10 == 0: + print(f" Batch {batch_idx+1}/{len(train_loader)}, Loss: {loss.item():.4f}") + + avg_train_loss = train_loss / len(train_loader) + print(f" Average Training Loss: {avg_train_loss:.4f}") + + # 验证步骤 + model.eval() + val_loss = 0.0 + dice_coeffs = [] + with torch.no_grad(): + for images, labels in val_loader: + images = images.to(DEVICE, dtype=torch.float) + labels = labels.to(DEVICE, dtype=torch.float) + + outputs = model(images) + loss = criterion(outputs, labels) + val_loss += loss.item() + + dice_coeff = calculate_dice_coefficient(outputs, labels) + dice_coeffs.append(dice_coeff) + + avg_val_loss = val_loss / len(val_loader) + avg_dice = sum(dice_coeffs) / len(dice_coeffs) + print(f" Validation Loss: {avg_val_loss:.4f}, Average Dice Coefficient: {avg_dice:.4f}") + + +# 验证数据加载 +for i in range(1): + image, label = train_dataset[i] + print(f"Sample {i}: Image shape: {image.shape}, Label shape: {label.shape}") + +# 开始训练 +if __name__ == "__main__": + train() \ No newline at end of file diff --git a/recognition/3D-UNT 48790835/utils.py b/recognition/3D-UNT 48790835/utils.py new file mode 100644 index 000000000..ee9920980 --- /dev/null +++ b/recognition/3D-UNT 48790835/utils.py @@ -0,0 +1,40 @@ +import torch +import torch.nn.functional as F + +class DiceLoss(torch.nn.Module): + def __init__(self, smooth=1e-5): + super(DiceLoss, self).__init__() + self.smooth = smooth + + def forward(self, outputs, targets): + num_classes = outputs.shape[1] + outputs = F.softmax(outputs, dim=1) + + # 将targets转换为one-hot编码 + targets_one_hot = F.one_hot(targets, num_classes=num_classes) + targets_one_hot = targets_one_hot.permute(0, 4, 1, 2, 3) # [B, C, D, H, W] + + outputs = outputs.contiguous().view(-1) + targets_one_hot = targets_one_hot.contiguous().view(-1) + + intersection = (outputs * targets_one_hot).sum() + dice = (2. * intersection + self.smooth) / (outputs.sum() + targets_one_hot.sum() + self.smooth) + loss = 1 - dice + + return loss + +def calculate_dice_coefficient(outputs, targets): + num_classes = outputs.shape[1] + outputs = F.softmax(outputs, dim=1) + preds = torch.argmax(outputs, dim=1) + + dice_score = 0.0 + for i in range(num_classes): + pred_i = (preds == i).float() + target_i = (targets == i).float() + intersection = (pred_i * target_i).sum() + union = pred_i.sum() + target_i.sum() + dice = (2. * intersection) / (union + 1e-5) + dice_score += dice + + return dice_score / num_classes \ No newline at end of file From 7f997a64386569b7aa28d9d83c305aec2e642c7a Mon Sep 17 00:00:00 2001 From: Han1zen Date: Wed, 23 Oct 2024 23:27:48 +1000 Subject: [PATCH 05/26] Delete recognition/3DUNET 48790835 --- recognition/3DUNET 48790835 | 1 - 1 file changed, 1 deletion(-) delete mode 100644 recognition/3DUNET 48790835 diff --git a/recognition/3DUNET 48790835 b/recognition/3DUNET 48790835 deleted file mode 100644 index 8b1378917..000000000 --- a/recognition/3DUNET 48790835 +++ /dev/null @@ -1 +0,0 @@ - From 357ed3ab466af73dfea147a6ebe7c52bbd8876a5 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Sat, 26 Oct 2024 22:18:11 +1000 Subject: [PATCH 06/26] Add files via upload --- recognition/3D-UNT 48790835/README.md | 54 +++++++ recognition/3D-UNT 48790835/dataset.py | 156 +++--------------- recognition/3D-UNT 48790835/modules.py | 94 ++++++----- recognition/3D-UNT 48790835/predict.py | 98 ++++-------- recognition/3D-UNT 48790835/train.py | 211 +++++++++++++++---------- 5 files changed, 288 insertions(+), 325 deletions(-) diff --git a/recognition/3D-UNT 48790835/README.md b/recognition/3D-UNT 48790835/README.md index 8b1378917..69b9cbe68 100644 --- a/recognition/3D-UNT 48790835/README.md +++ b/recognition/3D-UNT 48790835/README.md @@ -1 +1,55 @@ +# Medical Image Segmentation Project +## Project Overview +This project aims to utilize a 3D U-Net network for the segmentation of medical images. By loading MRI medical images and their corresponding labels, the model is trained and evaluated on a test set. + +## File Structure +Below are the main files in the project and their functionalities: + +1. **train.py**: + - This script is used for training the model. It loads the training dataset and labels, trains the 3D U-Net model using an optimizer and loss function, and performs validation after each training epoch. + - It uses a custom dataset `MedicalDataset3D` and implements Dice loss as the training objective. + +2. **dataset.py**: + - Contains the custom dataset class `MedicalDataset3D`, which is used to load 3D medical images and optional labels. + - Provides functionality for loading image paths and supports data augmentation. + +3. **utils.py**: + - Contains the `DiceLoss` class for calculating the Dice loss and functions for computing the Dice coefficient. + - This module is primarily used for calculating the model's accuracy during the evaluation process. + +4. **modules.py**: + - Defines the structure of the 3D U-Net model, including convolutional layers, activation layers, and upsampling layers. + - The model is designed to extract features from the input images to improve segmentation accuracy. + +5. **predict.py**: + - This script is used to load test data and apply the trained model for predictions. It calculates and prints the average Dice coefficient on the test data while visualizing the input images, target labels, and prediction results. + +## Usage Instructions + +### Environment Requirements +- Python 3.x +- PyTorch +- nibabel +- numpy +- tqdm +- matplotlib + +### Data Preparation +Before using this project, please ensure you have the following data prepared: +- 3D medical images (NIfTI format) +- Corresponding label data + +### Training the Model +1. Modify the data paths in `train.py` to point to your dataset. +2. Run `train.py` to start training. + +### Prediction +1. Set the path for the test data in `predict.py`. +2. Run `predict.py` to obtain prediction results and Dice coefficient evaluation. + +## Contribution +Contributions of any kind are welcome! If you have suggestions or issues, please open an issue or pull request. + +## License +This project is licensed under the MIT License. See the LICENSE file for details. \ No newline at end of file diff --git a/recognition/3D-UNT 48790835/dataset.py b/recognition/3D-UNT 48790835/dataset.py index 0cce4e84d..bc0b738f2 100644 --- a/recognition/3D-UNT 48790835/dataset.py +++ b/recognition/3D-UNT 48790835/dataset.py @@ -1,145 +1,41 @@ -import os import numpy as np import nibabel as nib -from tqdm import tqdm import torch from torch.utils.data import Dataset -from scipy.ndimage import zoom -def to_channels(arr: np.ndarray, dtype=np.uint8) -> np.ndarray: - """ - 将三维数组转换为带有通道的独热编码四维数组。 - """ - channels = np.unique(arr) - res = np.zeros(arr.shape + (len(channels),), dtype=dtype) - for idx, c in enumerate(channels): - res[..., idx][arr == c] = 1 - return res - -def applyOrientation(niftiImage, interpolation='linear', scale=1): - """ - 应用方向和缩放到 NIfTI 图像。 - - 参数: - - niftiImage:nibabel NIfTI 图像对象。 - - interpolation:插值方法,'linear' 或 'nearest'。 - - scale:缩放因子。 - """ - # 这里进行重采样或重新取样等操作 - # 由于具体实现取决于您的需求,以下是一个示例(需要根据实际情况修改): - data = niftiImage.get_fdata() - affine = niftiImage.affine - - # 进行缩放(如果需要) - if scale != 1: - zoom_factors = [scale, scale, scale] - data = zoom(data, zoom=zoom_factors, order=1 if interpolation == 'linear' else 0) - - # 创建新的 NIfTI 图像 - new_nifti = nib.Nifti1Image(data, affine) - return new_nifti - -class MedicalDataset3D(Dataset): - """ - 自定义数据集类,用于加载 3D 医学图像。 - """ - def __init__(self, image_paths, label_paths=None, transform=None, normImage=False, categorical=False, dtype=np.float32, orient=False): - """ - 参数: - - image_paths:图像文件路径列表。 - - label_paths:标签文件路径列表(如果有)。 - - transform:数据增强变换。 - - normImage:是否标准化图像。 - - categorical:是否将标签转换为独热编码。 - - dtype:数据类型。 - - orient:是否应用方向和缩放。 - """ - self.image_paths = image_paths - self.label_paths = label_paths +class NiftiDataset(Dataset): + """Nifti格式数据集""" + def __init__(self, image_files, label_files=None, transform=None): + self.image_files = image_files + self.label_files = label_files self.transform = transform - self.normImage = normImage - self.categorical = categorical - self.dtype = dtype - self.orient = orient + + # 确保图像和标签数量一致 + if self.label_files is not None: + assert len(self.image_files) == len(self.label_files), "图像和标签文件数量不一致!" def __len__(self): - return len(self.image_paths) + return len(self.image_files) def __getitem__(self, idx): # 加载图像 - image_nifti = nib.load(self.image_paths[idx]) - if self.orient: - image_nifti = applyOrientation(image_nifti, interpolation='linear', scale=1) - image = image_nifti.get_fdata(caching='unchanged') - if len(image.shape) == 4: - image = image[:, :, :, 0] - image = image.astype(self.dtype) - if self.normImage: - image = (image - image.mean()) / image.std() - image = np.expand_dims(image, axis=0) # 添加通道维度 - - # 如果有标签,加载标签 - if self.label_paths: - label_nifti = nib.load(self.label_paths[idx]) - if self.orient: - label_nifti = applyOrientation(label_nifti, interpolation='nearest', scale=1) - label = label_nifti.get_fdata(caching='unchanged') - if len(label.shape) == 4: - label = label[:, :, :, 0] - label = label.astype(self.dtype) - if self.categorical: - label = to_channels(label, dtype=self.dtype) - label = np.moveaxis(label, -1, 0) # 将通道维度移到前面 - else: - label = np.expand_dims(label, axis=0) # 添加通道维度 + image = nib.load(self.image_files[idx]).get_fdata().astype(np.float32) + # 处理异常值,避免NaN + image = np.nan_to_num(image) + # 标准化 + if np.std(image) != 0: + image = (image - np.mean(image)) / np.std(image) else: - label = None - - # 应用数据增强(如果有) - if self.transform: - # 注意,您需要确保 transform 适用于 3D 图像数据 - if label is not None: - data = {"image": image, "mask": label} - augmented = self.transform(**data) - image = augmented["image"] - label = augmented["mask"] - else: - data = {"image": image} - augmented = self.transform(**data) - image = augmented["image"] - + image = image - np.mean(image) # 转换为张量 - image = torch.tensor(image, dtype=torch.float32) - if label is not None: - label = torch.tensor(label, dtype=torch.float32) + image = torch.from_numpy(image).unsqueeze(0) + + if self.label_files is not None: + # 加载标签 + label = nib.load(self.label_files[idx]).get_fdata().astype(np.uint8) + label = np.nan_to_num(label) + # 转换为张量 + label = torch.from_numpy(label).long() return image, label else: - return image - -def load_image_paths(data_dir, split='train'): - """ - 获取图像和标签的文件路径列表。 - - 参数: - - data_dir:数据集目录。 - - split:数据集划分,'train'、'val' 或 'test'。 - - 返回: - - image_paths:图像文件路径列表。 - - label_paths:标签文件路径列表。 - """ - # 根据您的数据组织方式,实现获取文件路径的逻辑 - # dataset.py - - -def load_image_paths(image_dir, label_dir): - image_paths = sorted([ - os.path.join(image_dir, f) for f in os.listdir(image_dir) - if f.endswith('.nii') or f.endswith('.nii.gz') - ]) - label_paths = sorted([ - os.path.join(label_dir, f) for f in os.listdir(label_dir) - if f.endswith('.nii') or f.endswith('.nii.gz') - ]) - return image_paths, label_paths - + return image \ No newline at end of file diff --git a/recognition/3D-UNT 48790835/modules.py b/recognition/3D-UNT 48790835/modules.py index 0c70d2b15..c623bdf69 100644 --- a/recognition/3D-UNT 48790835/modules.py +++ b/recognition/3D-UNT 48790835/modules.py @@ -4,9 +4,7 @@ import torch.nn.functional as F class DoubleConv(nn.Module): - """ - 两次卷积操作:Conv3D => BN => ReLU => Conv3D => BN => ReLU - """ + """两次卷积操作:Conv3D -> ReLU -> Conv3D -> ReLU""" def __init__(self, in_channels, out_channels): super(DoubleConv, self).__init__() self.double_conv = nn.Sequential( @@ -21,51 +19,63 @@ def __init__(self, in_channels, out_channels): def forward(self, x): return self.double_conv(x) -class UNet3D(nn.Module): - def __init__(self, in_channels=1, out_channels=1, features=[32, 64, 128, 256]): - super(UNet3D, self).__init__() - self.ups = nn.ModuleList() - self.downs = nn.ModuleList() - self.pool = nn.MaxPool3d(kernel_size=2, stride=2) - - # 下采样部分 - for feature in features: - self.downs.append(DoubleConv(in_channels, feature)) - in_channels = feature +class ImprovedUNet3D(nn.Module): + """改进的3D UNet模型""" + def __init__(self, in_channels, out_channels): + super(ImprovedUNet3D, self).__init__() + self.in_channels = in_channels + self.out_channels = out_channels - # 上采样部分 - for feature in reversed(features): - self.ups.append( - nn.ConvTranspose3d(feature * 2, feature, kernel_size=2, stride=2) - ) - self.ups.append(DoubleConv(feature * 2, feature)) + self.down1 = DoubleConv(in_channels, 64) + self.pool1 = nn.MaxPool3d(2) + self.down2 = DoubleConv(64, 128) + self.pool2 = nn.MaxPool3d(2) + self.down3 = DoubleConv(128, 256) + self.pool3 = nn.MaxPool3d(2) + self.down4 = DoubleConv(256, 512) + self.pool4 = nn.MaxPool3d(2) - self.bottleneck = DoubleConv(features[-1], features[-1] * 2) - self.final_conv = nn.Conv3d(features[0], out_channels, kernel_size=1) + self.bottleneck = DoubleConv(512, 1024) - def forward(self, x): - skip_connections = [] + self.up1 = nn.ConvTranspose3d(1024, 512, kernel_size=2, stride=2) + self.conv1 = DoubleConv(1024, 512) + self.up2 = nn.ConvTranspose3d(512, 256, kernel_size=2, stride=2) + self.conv2 = DoubleConv(512, 256) + self.up3 = nn.ConvTranspose3d(256, 128, kernel_size=2, stride=2) + self.conv3 = DoubleConv(256, 128) + self.up4 = nn.ConvTranspose3d(128, 64, kernel_size=2, stride=2) + self.conv4 = DoubleConv(128, 64) - # 下采样路径 - for down in self.downs: - x = down(x) - skip_connections.append(x) - x = self.pool(x) + self.out_conv = nn.Conv3d(64, out_channels, kernel_size=1) - x = self.bottleneck(x) - skip_connections = skip_connections[::-1] - - # 上采样路径 - for idx in range(0, len(self.ups), 2): - x = self.ups[idx](x) - skip_connection = skip_connections[idx // 2] + def forward(self, x): + # 编码路径 + x1 = self.down1(x) + x2 = self.pool1(x1) + x3 = self.down2(x2) + x4 = self.pool2(x3) + x5 = self.down3(x4) + x6 = self.pool3(x5) + x7 = self.down4(x6) + x8 = self.pool4(x7) - # 如果尺寸不匹配,进行裁剪(可根据需要调整) - if x.shape != skip_connection.shape: - x = F.interpolate(x, size=skip_connection.shape[2:]) + # Bottle neck + x9 = self.bottleneck(x8) - concat_skip = torch.cat((skip_connection, x), dim=1) - x = self.ups[idx + 1](concat_skip) + # 解码路径 + x10 = self.up1(x9) + x10 = torch.cat([x7, x10], dim=1) + x11 = self.conv1(x10) + x12 = self.up2(x11) + x12 = torch.cat([x5, x12], dim=1) + x13 = self.conv2(x12) + x14 = self.up3(x13) + x14 = torch.cat([x3, x14], dim=1) + x15 = self.conv3(x14) + x16 = self.up4(x15) + x16 = torch.cat([x1, x16], dim=1) + x17 = self.conv4(x16) - return self.final_conv(x) + output = self.out_conv(x17) + return output diff --git a/recognition/3D-UNT 48790835/predict.py b/recognition/3D-UNT 48790835/predict.py index cd4738f26..bd497288d 100644 --- a/recognition/3D-UNT 48790835/predict.py +++ b/recognition/3D-UNT 48790835/predict.py @@ -1,76 +1,42 @@ import torch -from torch.utils.data import DataLoader -from modules import UNet3D -from dataset import MedicalDataset3D, load_image_paths -import matplotlib.pyplot as plt +from modules import ImprovedUNet3D +from dataset import NiftiDataset +import nibabel as nib import numpy as np +import matplotlib.pyplot as plt +from matplotlib import font_manager -DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' - -# 加载测试数据集 -# predict.py - -from dataset import MedicalDataset3D, load_image_paths - -# 设置数据路径 -data_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/data' - - -# 获取测试集的文件路径 -test_image_paths, test_label_paths = load_image_paths(data_dir, split='test') - -# 创建测试数据集 -test_dataset = MedicalDataset3D( - image_paths=test_image_paths, - label_paths=test_label_paths, - normImage=True, - categorical=False, - dtype=np.float32, - orient=True # 如果需要应用方向和缩放 -) - -# 创建数据加载器 -test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False) +# 设置字体 +font_path = '/mnt/data/file-ngwyeoEN29l1M3O1QpdxCwkj' +font_prop = font_manager.FontProperties(fname=font_path) -# 初始化模型并加载训练好的权重 -model = UNet3D(in_channels=1, out_channels=1).to(DEVICE) -model.load_state_dict(torch.load('checkpoint_epoch_50.pth', map_location=DEVICE)) +# 加载模型 +model = ImprovedUNet3D(in_channels=1, out_channels=2) +model.load_state_dict(torch.load('improved_unet3d.pth')) model.eval() -def dice_coefficient(preds, targets, smooth=1e-6): - preds = (preds > 0.5).float() - intersection = (preds * targets).sum() - union = preds.sum() + targets.sum() - dice = (2 * intersection + smooth) / (union + smooth) - return dice.item() - -dice_scores = [] +# 加载测试数据 +test_images = ['path_to_test_image1.nii', 'path_to_test_image2.nii'] # 请根据需要替换路径 +test_dataset = NiftiDataset(test_images) +test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, shuffle=False) +# 进行预测并可视化结果 with torch.no_grad(): - for data, targets in test_loader: - data = data.to(DEVICE, dtype=torch.float) - targets = targets.to(DEVICE, dtype=torch.float) - - predictions = model(data) - dice = dice_coefficient(torch.sigmoid(predictions), targets) - dice_scores.append(dice) - - # 可视化结果(仅展示中间的一张切片) - slice_idx = data.shape[2] // 2 - input_slice = data.cpu().numpy()[0, 0, slice_idx, :, :] - target_slice = targets.cpu().numpy()[0, 0, slice_idx, :, :] - pred_slice = torch.sigmoid(predictions).cpu().numpy()[0, 0, slice_idx, :, :] - - fig, axes = plt.subplots(1, 3, figsize=(12, 4)) - axes[0].imshow(input_slice, cmap='gray') - axes[0].set_title('Input') - axes[1].imshow(target_slice, cmap='gray') - axes[1].set_title('Ground Truth') - axes[2].imshow(pred_slice, cmap='gray') - axes[2].set_title('Prediction') + for idx, images in enumerate(test_loader): + outputs = model(images) + prediction = torch.argmax(outputs, dim=1).squeeze().numpy() + + # 可视化中间切片 + slice_idx = prediction.shape[2] // 2 + plt.figure(figsize=(12, 6)) + plt.subplot(1, 2, 1) + plt.imshow(images.squeeze().numpy()[:, :, slice_idx], cmap='gray') + plt.title('Input Image', fontproperties=font_prop) + plt.subplot(1, 2, 2) + plt.imshow(prediction[:, :, slice_idx], cmap='jet') + plt.title('Predicted Segmentation', fontproperties=font_prop) plt.show() -# 打印平均 Dice 系数 -avg_dice = np.mean(dice_scores) -print(f'Average Dice Similarity Coefficient on Test Set: {avg_dice:.4f}') - + # 保存预测结果为Nifti文件 + pred_img = nib.Nifti1Image(prediction.astype(np.uint8), affine=np.eye(4)) + nib.save(pred_img, f'prediction_{idx}.nii') \ No newline at end of file diff --git a/recognition/3D-UNT 48790835/train.py b/recognition/3D-UNT 48790835/train.py index 65188e2b9..c38328509 100644 --- a/recognition/3D-UNT 48790835/train.py +++ b/recognition/3D-UNT 48790835/train.py @@ -1,96 +1,133 @@ import os import torch +import torch.nn as nn from torch.utils.data import DataLoader -from sklearn.model_selection import train_test_split -from dataset import MedicalDataset3D, load_image_paths -from modules import UNet3D -from utils import DiceLoss, calculate_dice_coefficient - -# 设置超参数 -DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' -LEARNING_RATE = 1e-4 -BATCH_SIZE = 1 # 根据您的显存大小调整批量大小 -EPOCHS = 50 - -# 指定图像和标签目录 -image_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/' \ - 'Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/' \ - 'data/HipMRI_study_complete_release_v1/semantic_MRs_anon' - -label_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/' \ - 'Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/' \ - 'data/HipMRI_study_complete_release_v1/semantic_labels_anon' - - -# 加载图像和标签路径 -image_paths, label_paths = load_image_paths(image_dir, label_dir) - -# 将数据集划分为训练集和验证集 -train_images, val_images, train_labels, val_labels = train_test_split( - image_paths, label_paths, test_size=0.2, random_state=42) +from modules import ImprovedUNet3D +from dataset import NiftiDataset +import torch.optim as optim +import matplotlib.pyplot as plt +from matplotlib import font_manager + +# 设置字体 +font_path = '/mnt/data/file-ngwyeoEN29l1M3O1QpdxCwkj' +font_prop = font_manager.FontProperties(fname=font_path) + +# 定义超参数 +num_epochs = 50 +learning_rate = 0.001 +batch_size = 1 # 由于3D数据较大,batch_size设置为1,避免内存不足 + +# 定义文件夹路径 +labels_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/data/HipMRI_study_complete_release_v1/semantic_labels_anon' +mrs_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon' + +# 获取所有图像和标签文件 +train_images = sorted([f for f in os.listdir(mrs_dir) if f.endswith('.nii')]) +train_labels = sorted([f for f in os.listdir(labels_dir) if f.endswith('.nii')]) + +# 打印文件数量 +print(f"Total images: {len(train_images)}") +print(f"Total labels: {len(train_labels)}") + +train_images_filtered = [] +train_labels_filtered = [] + +# 匹配逻辑 +for image_name in train_images: + case_id = image_name.split('_')[0] + '_' + image_name.split('_')[1] # Case_ID + + found_match = False + for label_name in train_labels: + if case_id in label_name: + train_images_filtered.append(os.path.join(mrs_dir, image_name)) + train_labels_filtered.append(os.path.join(labels_dir, label_name)) + found_match = True + break + + if not found_match: + print(f"未找到匹配的标签文件:{image_name}") + +# 更新图像和标签文件列表 +train_images = train_images_filtered +train_labels = train_labels_filtered + +# 打印匹配结果 +print(f"匹配的图像数量: {len(train_images)}") +print(f"匹配的标签数量: {len(train_labels)}") + +# 断言检查 +assert len(train_images) == len(train_labels), "图像和标签文件数量不一致!" + +# 拆分训练集和验证集(90%训练,10%验证) +split_index = int(len(train_images) * 0.9) +train_images, val_images = train_images[:split_index], train_images[split_index:] +train_labels, val_labels = train_labels[:split_index], train_labels[split_index:] # 创建数据集和数据加载器 -train_dataset = MedicalDataset3D(train_images, train_labels) -val_dataset = MedicalDataset3D(val_images, val_labels) - -train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True) -val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False) - -# 初始化模型、损失函数和优化器 -model = UNet3D(in_channels=1, out_channels=1).to(DEVICE) -criterion = DiceLoss() -optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE) - -# 定义训练函数 -def train(): - for epoch in range(EPOCHS): - print(f"Epoch {epoch+1}/{EPOCHS}") - model.train() - train_loss = 0.0 - for batch_idx, (images, labels) in enumerate(train_loader): - images = images.to(DEVICE, dtype=torch.float) - labels = labels.to(DEVICE, dtype=torch.float) - - optimizer.zero_grad() - outputs = model(images) - loss = criterion(outputs, labels) - loss.backward() - optimizer.step() - - train_loss += loss.item() - - if (batch_idx + 1) % 10 == 0: - print(f" Batch {batch_idx+1}/{len(train_loader)}, Loss: {loss.item():.4f}") - - avg_train_loss = train_loss / len(train_loader) - print(f" Average Training Loss: {avg_train_loss:.4f}") - - # 验证步骤 - model.eval() - val_loss = 0.0 - dice_coeffs = [] - with torch.no_grad(): - for images, labels in val_loader: - images = images.to(DEVICE, dtype=torch.float) - labels = labels.to(DEVICE, dtype=torch.float) +train_dataset = NiftiDataset(train_images, train_labels) +val_dataset = NiftiDataset(val_images, val_labels) - outputs = model(images) - loss = criterion(outputs, labels) - val_loss += loss.item() +# 打印数据集大小 +print(f"Number of training samples: {len(train_dataset)}") +print(f"Number of validation samples: {len(val_dataset)}") - dice_coeff = calculate_dice_coefficient(outputs, labels) - dice_coeffs.append(dice_coeff) +train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) +val_loader = DataLoader(val_dataset, batch_size=1, shuffle=False) - avg_val_loss = val_loss / len(val_loader) - avg_dice = sum(dice_coeffs) / len(dice_coeffs) - print(f" Validation Loss: {avg_val_loss:.4f}, Average Dice Coefficient: {avg_dice:.4f}") - - -# 验证数据加载 -for i in range(1): - image, label = train_dataset[i] - print(f"Sample {i}: Image shape: {image.shape}, Label shape: {label.shape}") +# 初始化模型、损失函数和优化器 +model = ImprovedUNet3D(in_channels=1, out_channels=2) +criterion = nn.CrossEntropyLoss() +optimizer = optim.Adam(model.parameters(), lr=learning_rate) + +# 如果有GPU可用,使用GPU +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +model.to(device) + +# 训练和验证循环 +train_losses = [] +val_losses = [] + +for epoch in range(num_epochs): + model.train() + running_loss = 0.0 + for images, labels in train_loader: + images = images.to(device) + labels = labels.to(device) + + optimizer.zero_grad() # 清空梯度 + outputs = model(images) + loss = criterion(outputs, labels.long()) + loss.backward() + optimizer.step() + running_loss += loss.item() * images.size(0) + + epoch_loss = running_loss / len(train_loader.dataset) + train_losses.append(epoch_loss) + + model.eval() + val_running_loss = 0.0 + with torch.no_grad(): + for images, labels in val_loader: + images = images.to(device) + labels = labels.to(device) -# 开始训练 -if __name__ == "__main__": - train() \ No newline at end of file + outputs = model(images) + loss = criterion(outputs, labels.long()) + val_running_loss += loss.item() * images.size(0) + val_epoch_loss = val_running_loss / len(val_loader.dataset) + val_losses.append(val_epoch_loss) + + print(f"Epoch {epoch + 1}/{num_epochs}, Training Loss: {epoch_loss:.4f}, Validation Loss: {val_epoch_loss:.4f}") + +# 保存模型 +torch.save(model.state_dict(), 'improved_unet3d.pth') + +# 绘制损失曲线 +plt.figure() +plt.plot(range(1, num_epochs + 1), train_losses, label='Training Loss') +plt.plot(range(1, num_epochs + 1), val_losses, label='Validation Loss') +plt.xlabel('Epoch', fontproperties=font_prop) +plt.ylabel('Loss', fontproperties=font_prop) +plt.legend(prop=font_prop) +plt.savefig('loss_curve.png') +plt.close() \ No newline at end of file From 315abf0eed586576dda47f36fdbcc4c8b46e7ef2 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Sat, 26 Oct 2024 22:57:49 +1000 Subject: [PATCH 07/26] Add files via upload --- recognition/3D-UNT 48790835/train.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recognition/3D-UNT 48790835/train.py b/recognition/3D-UNT 48790835/train.py index c38328509..901fe739b 100644 --- a/recognition/3D-UNT 48790835/train.py +++ b/recognition/3D-UNT 48790835/train.py @@ -18,8 +18,8 @@ batch_size = 1 # 由于3D数据较大,batch_size设置为1,避免内存不足 # 定义文件夹路径 -labels_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/data/HipMRI_study_complete_release_v1/semantic_labels_anon' -mrs_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/未命名文件夹/Labelled_weekly_MR_images_of_the_male_pelvis-QEzDvqEq-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon' +labels_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon' +mrs_dir = '//Users/qiuhan/Desktop/UQ/3710/Lab3/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon' # 获取所有图像和标签文件 train_images = sorted([f for f in os.listdir(mrs_dir) if f.endswith('.nii')]) From f76a330abaf29496f0822c018f270cab6d55ef3b Mon Sep 17 00:00:00 2001 From: Han1zen Date: Sun, 27 Oct 2024 09:44:14 +1000 Subject: [PATCH 08/26] Add files via upload --- recognition/3D-UNT 48790835/dataset.py | 75 +++++---- recognition/3D-UNT 48790835/modules.py | 207 +++++++++++++++++-------- recognition/3D-UNT 48790835/predict.py | 99 +++++++----- recognition/3D-UNT 48790835/train.py | 204 ++++++++++++------------ 4 files changed, 346 insertions(+), 239 deletions(-) diff --git a/recognition/3D-UNT 48790835/dataset.py b/recognition/3D-UNT 48790835/dataset.py index bc0b738f2..c26c66e1f 100644 --- a/recognition/3D-UNT 48790835/dataset.py +++ b/recognition/3D-UNT 48790835/dataset.py @@ -1,41 +1,52 @@ -import numpy as np +import os +from torch.utils.data import Dataset +from torchvision.transforms import Compose import nibabel as nib +import numpy as np import torch -from torch.utils.data import Dataset -class NiftiDataset(Dataset): - """Nifti格式数据集""" - def __init__(self, image_files, label_files=None, transform=None): - self.image_files = image_files - self.label_files = label_files +# Define 3D medical dataset class +class Medical3DDataset(Dataset): + def __init__(self, imgs_path, labels_path, transform=None): + self.images_path = imgs_path + self.labels_path = labels_path + self.image_names = sorted(os.listdir(self.images_path)) + self.label_names = sorted(os.listdir(self.labels_path)) self.transform = transform - # 确保图像和标签数量一致 - if self.label_files is not None: - assert len(self.image_files) == len(self.label_files), "图像和标签文件数量不一致!" + if len(self.image_names) != len(self.label_names): + raise ValueError("The number of images and labels do not match. Please check the data.") def __len__(self): - return len(self.image_files) + return len(self.image_names) def __getitem__(self, idx): - # 加载图像 - image = nib.load(self.image_files[idx]).get_fdata().astype(np.float32) - # 处理异常值,避免NaN - image = np.nan_to_num(image) - # 标准化 - if np.std(image) != 0: - image = (image - np.mean(image)) / np.std(image) - else: - image = image - np.mean(image) - # 转换为张量 - image = torch.from_numpy(image).unsqueeze(0) - - if self.label_files is not None: - # 加载标签 - label = nib.load(self.label_files[idx]).get_fdata().astype(np.uint8) - label = np.nan_to_num(label) - # 转换为张量 - label = torch.from_numpy(label).long() - return image, label - else: - return image \ No newline at end of file + img_name = self.image_names[idx] + label_name = self.label_names[idx] + + img_path = os.path.join(self.images_path, img_name) + label_path = os.path.join(self.labels_path, label_name) + + # Load NIfTI images + image = nib.load(img_path).get_fdata().astype(np.float32) + label = nib.load(label_path).get_fdata().astype(np.float32) + + # Expand dimensions to match (C, D, H, W) format + image = np.expand_dims(image, axis=0) + label = np.expand_dims(label, axis=0) + + # Convert to Tensor + image = torch.from_numpy(image) + label = torch.from_numpy(label) + + if self.transform: + image = self.transform(image) + label = self.transform(label) + + return image, label + +# Define transforms for 3D images and labels if needed +def get_transform(): + return Compose([ + # Add 3D-specific transformations here if needed, like normalization, resizing, etc. + ]) diff --git a/recognition/3D-UNT 48790835/modules.py b/recognition/3D-UNT 48790835/modules.py index c623bdf69..6733907be 100644 --- a/recognition/3D-UNT 48790835/modules.py +++ b/recognition/3D-UNT 48790835/modules.py @@ -1,81 +1,152 @@ - import torch import torch.nn as nn -import torch.nn.functional as F - -class DoubleConv(nn.Module): - """两次卷积操作:Conv3D -> ReLU -> Conv3D -> ReLU""" - def __init__(self, in_channels, out_channels): - super(DoubleConv, self).__init__() - self.double_conv = nn.Sequential( - nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1), - nn.BatchNorm3d(out_channels), - nn.ReLU(inplace=True), - nn.Conv3d(out_channels, out_channels, kernel_size=3, padding=1), - nn.BatchNorm3d(out_channels), - nn.ReLU(inplace=True) - ) - def forward(self, x): - return self.double_conv(x) +# Hyper parameters +negativeSlope = 10 ** -2 +pDrop = 0.3 -class ImprovedUNet3D(nn.Module): - """改进的3D UNet模型""" - def __init__(self, in_channels, out_channels): - super(ImprovedUNet3D, self).__init__() + +class Improved3DUnet(nn.Module): + def __init__(self, in_channels=1, out_channels=1, features=[16, 32, 64, 128, 256]): + super(Improved3DUnet, self).__init__() self.in_channels = in_channels self.out_channels = out_channels + self.features = features + self.features_reversed = list(reversed(features)) + + self.lrelu = nn.LeakyReLU(negative_slope=negativeSlope) + self.dropout = nn.Dropout3d(p=pDrop) + self.upScale = nn.Upsample(scale_factor=2, mode='trilinear', align_corners=True) + self.softmax = nn.Softmax(dim=1) + + self.convs_context = nn.ModuleList() + self.contexts = nn.ModuleList() + self.norm_relus_context = nn.ModuleList() + self.convs_norm_relu_local = nn.ModuleList() + self.convs_local = nn.ModuleList() + self.upSamples = nn.ModuleList() - self.down1 = DoubleConv(in_channels, 64) - self.pool1 = nn.MaxPool3d(2) - self.down2 = DoubleConv(64, 128) - self.pool2 = nn.MaxPool3d(2) - self.down3 = DoubleConv(128, 256) - self.pool3 = nn.MaxPool3d(2) - self.down4 = DoubleConv(256, 512) - self.pool4 = nn.MaxPool3d(2) + for i in range(5): + if i == 0: + self.convs_context.append( + nn.Conv3d(self.in_channels, self.features[i], kernel_size=3, stride=1, padding=1, bias=False)) + self.convs_local.append( + nn.Conv3d(self.features_reversed[i + 1], self.features_reversed[i + 1], kernel_size=1, stride=1, + padding=0, bias=False)) + elif i == 4: + self.convs_context.append( + nn.Conv3d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) + self.convs_local.append( + nn.Conv3d(self.features_reversed[i - 1], self.out_channels, kernel_size=1, stride=1, padding=0, + bias=False)) + else: + self.convs_context.append( + nn.Conv3d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) + self.convs_local.append( + nn.Conv3d(self.features_reversed[i - 1], self.features_reversed[i], kernel_size=1, stride=1, + padding=0, bias=False)) - self.bottleneck = DoubleConv(512, 1024) + conv = self.norm_lrelu_conv(features[i], self.features[i]) + self.contexts.append(self.context(conv, conv)) + if i < 4: + norm_lrelu = self.norm_lrelu(self.features[i]) + self.norm_relus_context.append(norm_lrelu) - self.up1 = nn.ConvTranspose3d(1024, 512, kernel_size=2, stride=2) - self.conv1 = DoubleConv(1024, 512) - self.up2 = nn.ConvTranspose3d(512, 256, kernel_size=2, stride=2) - self.conv2 = DoubleConv(512, 256) - self.up3 = nn.ConvTranspose3d(256, 128, kernel_size=2, stride=2) - self.conv3 = DoubleConv(256, 128) - self.up4 = nn.ConvTranspose3d(128, 64, kernel_size=2, stride=2) - self.conv4 = DoubleConv(128, 64) + for p in range(4): + self.convs_norm_relu_local.append( + self.conv_norm_lrelu(self.features_reversed[p], self.features_reversed[p])) + self.upSamples.append(self.up_sample(self.features_reversed[p], self.features_reversed[p + 1])) - self.out_conv = nn.Conv3d(64, out_channels, kernel_size=1) + self.norm_local0 = nn.InstanceNorm3d(self.features_reversed[1]) + self.deep_segment_2_conv = nn.Conv3d(self.features_reversed[1], self.out_channels, kernel_size=1, stride=1, + padding=0, bias=False) + self.deep_segment_3_conv = nn.Conv3d(self.features_reversed[2], self.out_channels, kernel_size=1, stride=1, + padding=0, bias=False) + + def up_sample(self, feat_in, feat_out): + return nn.Sequential( + nn.InstanceNorm3d(feat_in), + self.lrelu, + self.upScale, + nn.Conv3d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), + nn.InstanceNorm3d(feat_out), + self.lrelu + ) + + def context(self, conv1, conv2): + return nn.Sequential( + conv1, + self.dropout, + conv2 + ) + + def norm_lrelu_conv(self, feat_in, feat_out): + return nn.Sequential( + nn.Conv3d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), + nn.InstanceNorm3d(feat_out), + self.lrelu + ) + + def norm_lrelu(self, feat): + return nn.Sequential( + nn.InstanceNorm3d(feat), + self.lrelu + ) + + def conv_norm_lrelu(self, feat_in, feat_out): + return nn.Sequential( + nn.Conv3d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), + nn.InstanceNorm3d(feat_out), + nn.LeakyReLU() + ) def forward(self, x): - # 编码路径 - x1 = self.down1(x) - x2 = self.pool1(x1) - x3 = self.down2(x2) - x4 = self.pool2(x3) - x5 = self.down3(x4) - x6 = self.pool3(x5) - x7 = self.down4(x6) - x8 = self.pool4(x7) - - # Bottle neck - x9 = self.bottleneck(x8) - - # 解码路径 - x10 = self.up1(x9) - x10 = torch.cat([x7, x10], dim=1) - x11 = self.conv1(x10) - x12 = self.up2(x11) - x12 = torch.cat([x5, x12], dim=1) - x13 = self.conv2(x12) - x14 = self.up3(x13) - x14 = torch.cat([x3, x14], dim=1) - x15 = self.conv3(x14) - x16 = self.up4(x15) - x16 = torch.cat([x1, x16], dim=1) - x17 = self.conv4(x16) - - output = self.out_conv(x17) - return output + residuals = dict() + skips = dict() + out = x + + # Contextualization level 1 to 5 + for i in range(5): + out = self.convs_context[i](out) + residuals[i] = out + out = self.contexts[i](out) + out += residuals[i] + if i < 4: + out = self.norm_relus_context[i](out) + skips[i] = out + + # Localization level 1 + out = self.upSamples[0](out) + out = self.convs_local[0](out) + out = self.norm_local0(out) + out = self.lrelu(out) + + # Localization level 2-5 + for j in range(4): + out = torch.cat([out, skips[3 - j]], dim=1) + out = self.convs_norm_relu_local[j](out) + if j == 1: + ds2 = out + elif j == 2: + ds3 = out + if j == 3: + out = self.convs_local[j + 1](out) + else: + out = self.convs_local[j + 1](out) + if j < 3: + out = self.upSamples[j + 1](out) + + # Segment layer summation + ds2_conv = self.deep_segment_2_conv(ds2) + ds2_conv_upscale = self.upScale(ds2_conv) + + ds3_conv = self.deep_segment_3_conv(ds3) + ds2_ds3_upscale = ds2_conv_upscale + ds3_conv + + ds2_ds3_upscale_upscale = self.upScale(ds2_ds3_upscale) + out += ds2_ds3_upscale_upscale + + # Sigmoid Layer + out = torch.sigmoid(out) + return out diff --git a/recognition/3D-UNT 48790835/predict.py b/recognition/3D-UNT 48790835/predict.py index bd497288d..56b53c0d2 100644 --- a/recognition/3D-UNT 48790835/predict.py +++ b/recognition/3D-UNT 48790835/predict.py @@ -1,42 +1,61 @@ import torch -from modules import ImprovedUNet3D -from dataset import NiftiDataset -import nibabel as nib -import numpy as np +from torch.utils.data import DataLoader import matplotlib.pyplot as plt -from matplotlib import font_manager - -# 设置字体 -font_path = '/mnt/data/file-ngwyeoEN29l1M3O1QpdxCwkj' -font_prop = font_manager.FontProperties(fname=font_path) - -# 加载模型 -model = ImprovedUNet3D(in_channels=1, out_channels=2) -model.load_state_dict(torch.load('improved_unet3d.pth')) -model.eval() - -# 加载测试数据 -test_images = ['path_to_test_image1.nii', 'path_to_test_image2.nii'] # 请根据需要替换路径 -test_dataset = NiftiDataset(test_images) -test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, shuffle=False) - -# 进行预测并可视化结果 -with torch.no_grad(): - for idx, images in enumerate(test_loader): - outputs = model(images) - prediction = torch.argmax(outputs, dim=1).squeeze().numpy() - - # 可视化中间切片 - slice_idx = prediction.shape[2] // 2 - plt.figure(figsize=(12, 6)) - plt.subplot(1, 2, 1) - plt.imshow(images.squeeze().numpy()[:, :, slice_idx], cmap='gray') - plt.title('Input Image', fontproperties=font_prop) - plt.subplot(1, 2, 2) - plt.imshow(prediction[:, :, slice_idx], cmap='jet') - plt.title('Predicted Segmentation', fontproperties=font_prop) - plt.show() - - # 保存预测结果为Nifti文件 - pred_img = nib.Nifti1Image(prediction.astype(np.uint8), affine=np.eye(4)) - nib.save(pred_img, f'prediction_{idx}.nii') \ No newline at end of file +from dataset import Medical3DDataset, get_transform +import modules +import train +import os +import time + +# Define paths for test data +TEST_IMAGES_PATH = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon" +TEST_LABELS_PATH = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon" + +def main(): + # Set device + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + if not torch.cuda.is_available(): + print("CUDA not available, using CPU") + + # Load test dataset + testDataSet = Medical3DDataset(TEST_IMAGES_PATH, TEST_LABELS_PATH, get_transform()) + testDataloader = DataLoader(testDataSet, batch_size=train.batchSize, shuffle=False) + + # Load trained model + model = modules.Improved2DUnet() + model.load_state_dict(torch.load(train.modelPath)) + model.to(device) + print("Model successfully loaded.") + + # Perform testing + test(testDataloader, model, device) + +def test(dataLoader, model, device): + losses_validation = [] + dice_similarities_validation = [] + + print("> Test inference started") + start = time.time() + model.eval() + with torch.no_grad(): + for step, (images, labels) in enumerate(dataLoader): + images = images.to(device) + labels = labels.to(device) + + # Get model outputs + outputs = model(images) + losses_validation.append(train.dice_loss(outputs, labels).item()) + dice_similarities_validation.append(train.dice_coefficient(outputs, labels).item()) + + # Save segmentations for the first batch + if step == 0: + train.save_segments(images, labels, outputs, numComparisons=9, test=True) + + print(f'Test Loss: {train.get_average(losses_validation):.5f}, ' + f'Test Average Dice Similarity: {train.get_average(dice_similarities_validation):.5f}') + end = time.time() + elapsed = end - start + print(f"Test inference took {elapsed/60:.2f} minutes in total") + +if __name__ == "__main__": + main() diff --git a/recognition/3D-UNT 48790835/train.py b/recognition/3D-UNT 48790835/train.py index 901fe739b..46345dcac 100644 --- a/recognition/3D-UNT 48790835/train.py +++ b/recognition/3D-UNT 48790835/train.py @@ -1,133 +1,139 @@ import os +import dataset +import modules +import matplotlib.pyplot as plt +from torch.utils.data import DataLoader +from tqdm import tqdm + import torch import torch.nn as nn -from torch.utils.data import DataLoader -from modules import ImprovedUNet3D -from dataset import NiftiDataset import torch.optim as optim -import matplotlib.pyplot as plt -from matplotlib import font_manager +import time +import numpy as np + +# Hyper-parameters +num_epochs = 30 +learning_rate = 5 * 10 ** -4 +batch_size = 4 +learning_rate_decay = 0.985 -# 设置字体 -font_path = '/mnt/data/file-ngwyeoEN29l1M3O1QpdxCwkj' -font_prop = font_manager.FontProperties(fname=font_path) -# 定义超参数 -num_epochs = 50 -learning_rate = 0.001 -batch_size = 1 # 由于3D数据较大,batch_size设置为1,避免内存不足 +validation_images_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon" +train_images_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon" +validation_labels_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon" +train_labels_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon" +model_path = "3d_unet_model.pt" -# 定义文件夹路径 -labels_dir = '/Users/qiuhan/Desktop/UQ/3710/Lab3/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon' -mrs_dir = '//Users/qiuhan/Desktop/UQ/3710/Lab3/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon' -# 获取所有图像和标签文件 -train_images = sorted([f for f in os.listdir(mrs_dir) if f.endswith('.nii')]) -train_labels = sorted([f for f in os.listdir(labels_dir) if f.endswith('.nii')]) -# 打印文件数量 -print(f"Total images: {len(train_images)}") -print(f"Total labels: {len(train_labels)}") +def init(): + transform = dataset.get_transform() + valid_dataset = dataset.Medical3DDataset(validation_images_path, validation_labels_path, transform=transform) + train_dataset = dataset.Medical3DDataset(train_images_path, train_labels_path, transform=transform) -train_images_filtered = [] -train_labels_filtered = [] + valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False) + train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) -# 匹配逻辑 -for image_name in train_images: - case_id = image_name.split('_')[0] + '_' + image_name.split('_')[1] # Case_ID + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + if not torch.cuda.is_available(): + print("Warning: CUDA not found. Using CPU") - found_match = False - for label_name in train_labels: - if case_id in label_name: - train_images_filtered.append(os.path.join(mrs_dir, image_name)) - train_labels_filtered.append(os.path.join(labels_dir, label_name)) - found_match = True - break + data_loaders = {'train': train_loader, 'valid': valid_loader} + return data_loaders, device - if not found_match: - print(f"未找到匹配的标签文件:{image_name}") -# 更新图像和标签文件列表 -train_images = train_images_filtered -train_labels = train_labels_filtered +def main(): + data_loaders, device = init() + model = modules.Improved3DUnet() + model = model.to(device) + train_and_validate(data_loaders, model, device) + torch.save(model.state_dict(), model_path) -# 打印匹配结果 -print(f"匹配的图像数量: {len(train_images)}") -print(f"匹配的标签数量: {len(train_labels)}") -# 断言检查 -assert len(train_images) == len(train_labels), "图像和标签文件数量不一致!" +def train_and_validate(data_loaders, model, device): + criterion = dice_loss + optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5) + scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=learning_rate_decay) -# 拆分训练集和验证集(90%训练,10%验证) -split_index = int(len(train_images) * 0.9) -train_images, val_images = train_images[:split_index], train_images[split_index:] -train_labels, val_labels = train_labels[:split_index], train_labels[split_index:] + train_losses, train_dice, val_losses, val_dice = [], [], [], [] -# 创建数据集和数据加载器 -train_dataset = NiftiDataset(train_images, train_labels) -val_dataset = NiftiDataset(val_images, val_labels) + for epoch in range(num_epochs): + train_loss, train_coeff = train(data_loaders["train"], model, device, criterion, optimizer) + val_loss, val_coeff = validate(data_loaders["valid"], model, device, criterion) -# 打印数据集大小 -print(f"Number of training samples: {len(train_dataset)}") -print(f"Number of validation samples: {len(val_dataset)}") + train_losses.append(train_loss) + train_dice.append(train_coeff) + val_losses.append(val_loss) + val_dice.append(val_coeff) -train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) -val_loader = DataLoader(val_dataset, batch_size=1, shuffle=False) + scheduler.step() -# 初始化模型、损失函数和优化器 -model = ImprovedUNet3D(in_channels=1, out_channels=2) -criterion = nn.CrossEntropyLoss() -optimizer = optim.Adam(model.parameters(), lr=learning_rate) + print(f"Epoch [{epoch + 1}/{num_epochs}], Training Loss: {train_loss:.5f}, Training Dice: {train_coeff:.5f}") + print(f"Validation Loss: {val_loss:.5f}, Validation Dice: {val_coeff:.5f}") -# 如果有GPU可用,使用GPU -device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -model.to(device) + save_plot(train_losses, val_losses, "Loss", "LossCurve.png") + save_plot(train_dice, val_dice, "Dice Coefficient", "DiceCurve.png") -# 训练和验证循环 -train_losses = [] -val_losses = [] -for epoch in range(num_epochs): +def train(loader, model, device, criterion, optimizer): model.train() - running_loss = 0.0 - for images, labels in train_loader: - images = images.to(device) - labels = labels.to(device) + losses, dice_scores = [], [] - optimizer.zero_grad() # 清空梯度 + for images, labels in loader: + images, labels = images.to(device), labels.to(device) outputs = model(images) - loss = criterion(outputs, labels.long()) + + loss = criterion(outputs, labels) + losses.append(loss.item()) + + dice_score = dice_coefficient(outputs, labels).item() + dice_scores.append(dice_score) + + optimizer.zero_grad() loss.backward() optimizer.step() - running_loss += loss.item() * images.size(0) - epoch_loss = running_loss / len(train_loader.dataset) - train_losses.append(epoch_loss) + return np.mean(losses), np.mean(dice_scores) + +def validate(loader, model, device, criterion): model.eval() - val_running_loss = 0.0 - with torch.no_grad(): - for images, labels in val_loader: - images = images.to(device) - labels = labels.to(device) + losses, dice_scores = [], [] + with torch.no_grad(): + for images, labels in loader: + images, labels = images.to(device), labels.to(device) outputs = model(images) - loss = criterion(outputs, labels.long()) - val_running_loss += loss.item() * images.size(0) - val_epoch_loss = val_running_loss / len(val_loader.dataset) - val_losses.append(val_epoch_loss) - - print(f"Epoch {epoch + 1}/{num_epochs}, Training Loss: {epoch_loss:.4f}, Validation Loss: {val_epoch_loss:.4f}") - -# 保存模型 -torch.save(model.state_dict(), 'improved_unet3d.pth') - -# 绘制损失曲线 -plt.figure() -plt.plot(range(1, num_epochs + 1), train_losses, label='Training Loss') -plt.plot(range(1, num_epochs + 1), val_losses, label='Validation Loss') -plt.xlabel('Epoch', fontproperties=font_prop) -plt.ylabel('Loss', fontproperties=font_prop) -plt.legend(prop=font_prop) -plt.savefig('loss_curve.png') -plt.close() \ No newline at end of file + + loss = criterion(outputs, labels) + losses.append(loss.item()) + + dice_score = dice_coefficient(outputs, labels).item() + dice_scores.append(dice_score) + + return np.mean(losses), np.mean(dice_scores) + + +def dice_loss(outputs, labels): + return 1 - dice_coefficient(outputs, labels) + + +def dice_coefficient(outputs, labels, epsilon=1e-8): + intersection = (outputs * labels).sum() + return (2. * intersection) / ((outputs + labels).sum() + epsilon) + + +def save_plot(train_list, val_list, metric, path): + epochs = list(range(1, num_epochs + 1)) + plt.plot(epochs, train_list, label=f"Training {metric}") + plt.plot(epochs, val_list, label=f"Validation {metric}") + plt.xlabel("Epochs") + plt.ylabel(metric) + plt.legend() + plt.title(f"Training and Validation {metric} Over Epochs") + plt.savefig(path) + plt.close() + + +if __name__ == "__main__": + main() From 1e07f85acc7f32c924a8d1212d2d7cc13087f4ce Mon Sep 17 00:00:00 2001 From: Han1zen Date: Sun, 27 Oct 2024 22:11:04 +1000 Subject: [PATCH 09/26] Add files via upload --- recognition/3D-UNT 48790835/dataset.py | 102 +++++++---- recognition/3D-UNT 48790835/modules.py | 115 ++++++------ recognition/3D-UNT 48790835/predict.py | 57 +++--- recognition/3D-UNT 48790835/train.py | 235 ++++++++++++++++++------- 4 files changed, 317 insertions(+), 192 deletions(-) diff --git a/recognition/3D-UNT 48790835/dataset.py b/recognition/3D-UNT 48790835/dataset.py index c26c66e1f..ec625dcb2 100644 --- a/recognition/3D-UNT 48790835/dataset.py +++ b/recognition/3D-UNT 48790835/dataset.py @@ -1,52 +1,80 @@ -import os -from torch.utils.data import Dataset -from torchvision.transforms import Compose -import nibabel as nib -import numpy as np import torch +import torch.nn as nn +import torch.nn.functional as F +import torchvision +import torchvision.transforms as transforms +from torch.utils.data import Dataset +from torchvision import datasets +from torchvision.transforms import ToTensor +from torchvision.io import read_image -# Define 3D medical dataset class -class Medical3DDataset(Dataset): - def __init__(self, imgs_path, labels_path, transform=None): - self.images_path = imgs_path - self.labels_path = labels_path - self.image_names = sorted(os.listdir(self.images_path)) - self.label_names = sorted(os.listdir(self.labels_path)) - self.transform = transform +import time +import math +import matplotlib.pyplot as plt +import os - if len(self.image_names) != len(self.label_names): - raise ValueError("The number of images and labels do not match. Please check the data.") - def __len__(self): - return len(self.image_names) - def __getitem__(self, idx): - img_name = self.image_names[idx] - label_name = self.label_names[idx] +# create hyper paramaters +new_size = 128 - img_path = os.path.join(self.images_path, img_name) - label_path = os.path.join(self.labels_path, label_name) +# create dataset class to store all the images +class ISIC2018DataSet(Dataset): + def __init__(self, imgs_path, labels_path, transform=None, labelTransform=None): + self.LabelsPath = labels_path + self.ImagesPath = imgs_path + self.LabelNames = os.listdir(self.LabelsPath) + self.imageNames = os.listdir(self.ImagesPath) + self.LabelsSize = len(self.LabelNames) + self.ImagesSize = len(self.imageNames) + self.transform = transform + self.labelTransform = labelTransform - # Load NIfTI images - image = nib.load(img_path).get_fdata().astype(np.float32) - label = nib.load(label_path).get_fdata().astype(np.float32) + def __len__(self): + if self.ImagesSize != self.LabelsSize: + print("Bad Data! Please Check Data, or unpredictable behaviour!") + return -1 + else: + return self.ImagesSize - # Expand dimensions to match (C, D, H, W) format - image = np.expand_dims(image, axis=0) - label = np.expand_dims(label, axis=0) + def __getitem__(self, idx): + img_path = os.path.join(self.ImagesPath, self.imageNames[idx]) - # Convert to Tensor - image = torch.from_numpy(image) - label = torch.from_numpy(label) + # This accounts for an invisible .db file in the test folder that can't be removed + if img_path == "isic_data/ISIC2018_Task1-2_Test_Input\Thumbs.db": + img_path = "isic_data/ISIC2018_Task1-2_Test_Input\ISIC_0036309.jpg" + self.imageNames[idx] = "ISIC_0036309.jpg" + image = read_image(img_path) + label_path = os.path.join(self.LabelsPath, self.imageNames[idx].removesuffix(".jpg") + "_segmentation.png") + label = read_image(label_path) if self.transform: image = self.transform(image) - label = self.transform(label) - + if self.labelTransform: + label = self.labelTransform(label) + return image, label -# Define transforms for 3D images and labels if needed -def get_transform(): - return Compose([ - # Add 3D-specific transformations here if needed, like normalization, resizing, etc. + + + +# functions to transform the images +def img_transform(): + + img_tr = transforms.Compose([ + transforms.ToPILImage(), + transforms.ToTensor(), + transforms.Resize((new_size, new_size)) ]) + + return img_tr + +def label_transform(): + + label_tr = transforms.Compose([ + transforms.ToPILImage(), + transforms.ToTensor(), + transforms.Resize((new_size, new_size)) + ]) + + return label_tr diff --git a/recognition/3D-UNT 48790835/modules.py b/recognition/3D-UNT 48790835/modules.py index 6733907be..bee5d9733 100644 --- a/recognition/3D-UNT 48790835/modules.py +++ b/recognition/3D-UNT 48790835/modules.py @@ -1,22 +1,24 @@ +# source code of the module components, enhanced unet is 6 years old so there should be plenty of literature to build it + import torch import torch.nn as nn # Hyper parameters -negativeSlope = 10 ** -2 +negativeSlope = 10**-2 pDrop = 0.3 +class Improved2DUnet(nn.Module): + def __init__(self, in_channels = 3, out_channels = 1, features = [16, 32, 64, 128, 256]): -class Improved3DUnet(nn.Module): - def __init__(self, in_channels=1, out_channels=1, features=[16, 32, 64, 128, 256]): - super(Improved3DUnet, self).__init__() + super(Improved2DUnet, self).__init__() self.in_channels = in_channels self.out_channels = out_channels self.features = features self.features_reversed = list(reversed(features)) self.lrelu = nn.LeakyReLU(negative_slope=negativeSlope) - self.dropout = nn.Dropout3d(p=pDrop) - self.upScale = nn.Upsample(scale_factor=2, mode='trilinear', align_corners=True) + self.dropout = nn.Dropout2d(p=pDrop) + self.upScale = nn.Upsample(scale_factor=2, mode='nearest') self.softmax = nn.Softmax(dim=1) self.convs_context = nn.ModuleList() @@ -28,48 +30,36 @@ def __init__(self, in_channels=1, out_channels=1, features=[16, 32, 64, 128, 256 for i in range(5): if i == 0: - self.convs_context.append( - nn.Conv3d(self.in_channels, self.features[i], kernel_size=3, stride=1, padding=1, bias=False)) - self.convs_local.append( - nn.Conv3d(self.features_reversed[i + 1], self.features_reversed[i + 1], kernel_size=1, stride=1, - padding=0, bias=False)) + self.convs_context.append(nn.Conv2d(self.in_channels, self.features[i], kernel_size=3, stride=1, padding=1, bias=False)) + self.convs_local.append(nn.Conv2d(self.features_reversed[i + 1], self.features_reversed[i + 1], kernel_size=1, stride=1, padding=0, bias=False)) elif i == 4: - self.convs_context.append( - nn.Conv3d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) - self.convs_local.append( - nn.Conv3d(self.features_reversed[i - 1], self.out_channels, kernel_size=1, stride=1, padding=0, - bias=False)) + self.convs_context.append(nn.Conv2d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) + self.convs_local.append(nn.Conv2d(self.features_reversed[i - 1], self.out_channels, kernel_size=1, stride=1, padding=0, bias=False)) else: - self.convs_context.append( - nn.Conv3d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) - self.convs_local.append( - nn.Conv3d(self.features_reversed[i - 1], self.features_reversed[i], kernel_size=1, stride=1, - padding=0, bias=False)) - + self.convs_context.append(nn.Conv2d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) + self.convs_local.append(nn.Conv2d(self.features_reversed[i - 1], self.features_reversed[i], kernel_size=1, stride=1, padding=0, bias=False)) + conv = self.norm_lrelu_conv(features[i], self.features[i]) self.contexts.append(self.context(conv, conv)) if i < 4: norm_lrelu = self.norm_lrelu(self.features[i]) self.norm_relus_context.append(norm_lrelu) - + for p in range(4): - self.convs_norm_relu_local.append( - self.conv_norm_lrelu(self.features_reversed[p], self.features_reversed[p])) + self.convs_norm_relu_local.append(self.conv_norm_lrelu(self.features_reversed[p], self.features_reversed[p])) self.upSamples.append(self.up_sample(self.features_reversed[p], self.features_reversed[p + 1])) - - self.norm_local0 = nn.InstanceNorm3d(self.features_reversed[1]) - self.deep_segment_2_conv = nn.Conv3d(self.features_reversed[1], self.out_channels, kernel_size=1, stride=1, - padding=0, bias=False) - self.deep_segment_3_conv = nn.Conv3d(self.features_reversed[2], self.out_channels, kernel_size=1, stride=1, - padding=0, bias=False) + + self.norm_local0 = nn.InstanceNorm2d(self.features_reversed[1]) + self.deep_segment_2_conv = nn.Conv2d(self.features_reversed[1], self.out_channels, kernel_size=1, stride=1, padding=0, bias=False) + self.deep_segment_3_conv = nn.Conv2d(self.features_reversed[2], self.out_channels, kernel_size=1, stride=1, padding=0, bias=False) def up_sample(self, feat_in, feat_out): return nn.Sequential( - nn.InstanceNorm3d(feat_in), + nn.InstanceNorm2d(feat_in), self.lrelu, self.upScale, - nn.Conv3d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), - nn.InstanceNorm3d(feat_out), + nn.Conv2d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), + nn.InstanceNorm2d(feat_out), self.lrelu ) @@ -82,61 +72,64 @@ def context(self, conv1, conv2): def norm_lrelu_conv(self, feat_in, feat_out): return nn.Sequential( - nn.Conv3d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), - nn.InstanceNorm3d(feat_out), - self.lrelu - ) + nn.Conv2d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), + nn.InstanceNorm2d(feat_out), + self.lrelu) def norm_lrelu(self, feat): return nn.Sequential( - nn.InstanceNorm3d(feat), + nn.InstanceNorm2d(feat), self.lrelu ) def conv_norm_lrelu(self, feat_in, feat_out): return nn.Sequential( - nn.Conv3d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), - nn.InstanceNorm3d(feat_out), - nn.LeakyReLU() - ) - + nn.Conv2d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), + nn.InstanceNorm2d(feat_out), + nn.LeakyReLU()) + def forward(self, x): residuals = dict() skips = dict() out = x - - # Contextualization level 1 to 5 + + #Contextualization level 1 to 5 for i in range(5): out = self.convs_context[i](out) residuals[i] = out out = self.contexts[i](out) out += residuals[i] - if i < 4: + if (i < 4): out = self.norm_relus_context[i](out) skips[i] = out + + + # localization level 1 - # Localization level 1 out = self.upSamples[0](out) out = self.convs_local[0](out) out = self.norm_local0(out) out = self.lrelu(out) - + # Localization level 2-5 + for j in range(4): - out = torch.cat([out, skips[3 - j]], dim=1) + out = torch.cat([out, skips[3-j]], dim=1) out = self.convs_norm_relu_local[j](out) - if j == 1: + if (j == 1): ds2 = out - elif j == 2: + elif (j == 2): ds3 = out - if j == 3: - out = self.convs_local[j + 1](out) + if (j == 3): + out = self.convs_local[j+1](out) else: - out = self.convs_local[j + 1](out) - if j < 3: - out = self.upSamples[j + 1](out) + out = self.convs_local[j+1](out) + if (j < 3): + out = self.upSamples[j+1](out) + + + #segment layer summation - # Segment layer summation ds2_conv = self.deep_segment_2_conv(ds2) ds2_conv_upscale = self.upScale(ds2_conv) @@ -145,8 +138,10 @@ def forward(self, x): ds2_ds3_upscale_upscale = self.upScale(ds2_ds3_upscale) out += ds2_ds3_upscale_upscale + + #Sigmoid Layer instead of softmax, justified in above docstring. - # Sigmoid Layer - out = torch.sigmoid(out) + out= torch.sigmoid(out) + return out diff --git a/recognition/3D-UNT 48790835/predict.py b/recognition/3D-UNT 48790835/predict.py index 56b53c0d2..597c0d76f 100644 --- a/recognition/3D-UNT 48790835/predict.py +++ b/recognition/3D-UNT 48790835/predict.py @@ -1,61 +1,62 @@ -import torch -from torch.utils.data import DataLoader -import matplotlib.pyplot as plt -from dataset import Medical3DDataset, get_transform +import dataset import modules import train -import os +import matplotlib.pyplot as plt +from torch.utils.data import DataLoader +from tqdm import tqdm + +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +import torchvision +import torchvision.transforms as transforms import time -# Define paths for test data -TEST_IMAGES_PATH = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon" -TEST_LABELS_PATH = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon" +# Add your own paths here +testImagesPath = "isic_data/ISIC2018_Task1-2_Test_Input" +testLabelsPath = "isic_data/ISIC2018_Task1_Test_GroundTruth" def main(): - # Set device device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') if not torch.cuda.is_available(): - print("CUDA not available, using CPU") + print("Warning CUDA not Found. Using CPU") - # Load test dataset - testDataSet = Medical3DDataset(TEST_IMAGES_PATH, TEST_LABELS_PATH, get_transform()) + testDataSet = dataset.ISIC2017DataSet(testImagesPath, testLabelsPath, dataset.ISIC_transform_img(), dataset.ISIC_transform_label()) testDataloader = DataLoader(testDataSet, batch_size=train.batchSize, shuffle=False) - # Load trained model model = modules.Improved2DUnet() model.load_state_dict(torch.load(train.modelPath)) model.to(device) - print("Model successfully loaded.") - - # Perform testing + print("Model Successfully Loaded") + test(testDataloader, model, device) def test(dataLoader, model, device): - losses_validation = [] - dice_similarities_validation = [] + losses_validation = list() + dice_similarities_validation = list() - print("> Test inference started") + print("> Test Inference Commenced") start = time.time() model.eval() with torch.no_grad(): + print(dataLoader) for step, (images, labels) in enumerate(dataLoader): + print(step) images = images.to(device) labels = labels.to(device) - # Get model outputs outputs = model(images) - losses_validation.append(train.dice_loss(outputs, labels).item()) - dice_similarities_validation.append(train.dice_coefficient(outputs, labels).item()) + losses_validation.append(train.dice_loss(outputs, labels)) + dice_similarities_validation.append(train.dice_coefficient(outputs, labels)) - # Save segmentations for the first batch - if step == 0: - train.save_segments(images, labels, outputs, numComparisons=9, test=True) + if (step == 0): + train.save_segments(images, labels, outputs, 9, test=True) - print(f'Test Loss: {train.get_average(losses_validation):.5f}, ' - f'Test Average Dice Similarity: {train.get_average(dice_similarities_validation):.5f}') + print('Test Loss: {:.5f}, Test Average Dice Similarity: {:.5f}'.format(train.get_average(losses_validation) ,train.get_average(dice_similarities_validation))) end = time.time() elapsed = end - start - print(f"Test inference took {elapsed/60:.2f} minutes in total") + print("Test Inference took " + str(elapsed/60) + " mins in total") if __name__ == "__main__": main() diff --git a/recognition/3D-UNT 48790835/train.py b/recognition/3D-UNT 48790835/train.py index 46345dcac..6b1f7da01 100644 --- a/recognition/3D-UNT 48790835/train.py +++ b/recognition/3D-UNT 48790835/train.py @@ -1,4 +1,4 @@ -import os +# source code for training, validating, testing and saving the model import dataset import modules import matplotlib.pyplot as plt @@ -7,133 +7,234 @@ import torch import torch.nn as nn +import torch.nn.functional as F import torch.optim as optim +import torchvision +import torchvision.transforms as transforms import time import numpy as np # Hyper-parameters num_epochs = 30 -learning_rate = 5 * 10 ** -4 -batch_size = 4 +learning_rate = 5 * 10**-4 +batchSize = 16 learning_rate_decay = 0.985 +# set up the funcitonality from the imported dataset.py and modules.py -validation_images_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon" -train_images_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_MRs_anon" -validation_labels_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon" -train_labels_path = "/Users/qiuhan/Desktop/UQ/3710/Improved-UNET-s4879083/重新下载的数据集/Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-/data/HipMRI_study_complete_release_v1/semantic_labels_anon" -model_path = "3d_unet_model.pt" - - +# Add your own paths here +validationImagesPath = "isic_data/ISIC2018_Task1-2_Validation_Input" +trainImagesPath = "isic_data/ISIC2018_Task1-2_Training_Input_x2" +validationLabelsPath = "isic_data/ISIC2018_Task1_Validation_GroundTruth" +trainLabelsPath = "isic_data/ISIC2018_Task1_Training_GroundTruth_x2" +modelPath = "model.pt" def init(): - transform = dataset.get_transform() - valid_dataset = dataset.Medical3DDataset(validation_images_path, validation_labels_path, transform=transform) - train_dataset = dataset.Medical3DDataset(train_images_path, train_labels_path, transform=transform) - - valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False) - train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) + validDataSet = dataset.ISIC2018DataSet(validationImagesPath, validationLabelsPath, dataset.img_transform(), dataset.label_transform()) + validDataloader = DataLoader(validDataSet, batch_size=batchSize, shuffle=False) + trainDataSet = dataset.ISIC2018DataSet(trainImagesPath, trainLabelsPath, dataset.img_transform(), dataset.label_transform()) + trainDataloader = DataLoader(trainDataSet, batch_size=batchSize, shuffle=True) + # Device configuration device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') if not torch.cuda.is_available(): - print("Warning: CUDA not found. Using CPU") + print("Warning CUDA not Found. Using CPU") - data_loaders = {'train': train_loader, 'valid': valid_loader} - return data_loaders, device + dataLoaders = dict() + dataLoaders["valid"] = validDataloader + dataLoaders["train"] = trainDataloader + dataSets = dict() + dataSets["valid"] = validDataSet + dataSets["train"] = trainDataSet + + return dataSets, dataLoaders, device def main(): - data_loaders, device = init() - model = modules.Improved3DUnet() + dataSets, dataLoaders, device = init() + model = modules.Improved2DUnet() model = model.to(device) - train_and_validate(data_loaders, model, device) - torch.save(model.state_dict(), model_path) + # training and validating + train_and_validate(dataLoaders, model, device) + + # saving + torch.save(model.state_dict(), modelPath) -def train_and_validate(data_loaders, model, device): +def train_and_validate(dataLoaders, model, device): + # Define optimization parameters and loss according to Improved Unet Paper. criterion = dice_loss - optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5) - scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=learning_rate_decay) + optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=10**-5) + scheduler = optim.lr_scheduler.ExponentialLR(optimizer=optimizer, gamma=learning_rate_decay) - train_losses, train_dice, val_losses, val_dice = [], [], [], [] + losses_training = list() + dice_similarities_training = list() + losses_valid = list() + dice_similarities_valid = list() + + print("Training and Validation Commenced:") + start = time.time() + epochNumber = 0 for epoch in range(num_epochs): - train_loss, train_coeff = train(data_loaders["train"], model, device, criterion, optimizer) - val_loss, val_coeff = validate(data_loaders["valid"], model, device, criterion) + epochNumber += 1 + train_loss, train_coeff = train(dataLoaders["train"], model, device, criterion, optimizer, scheduler) + valid_loss, valid_coeff = validate(dataLoaders["valid"], model, device, criterion, epochNumber) - train_losses.append(train_loss) - train_dice.append(train_coeff) - val_losses.append(val_loss) - val_dice.append(val_coeff) + losses_training.append(train_loss) + dice_similarities_training.append(train_coeff) + losses_valid.append(valid_loss) + dice_similarities_valid.append(valid_coeff) - scheduler.step() - print(f"Epoch [{epoch + 1}/{num_epochs}], Training Loss: {train_loss:.5f}, Training Dice: {train_coeff:.5f}") - print(f"Validation Loss: {val_loss:.5f}, Validation Dice: {val_coeff:.5f}") + print ("Epoch [{}/{}], Training Loss: {:.5f}, Training Dice Similarity {:.5f}".format(epoch+1, num_epochs, losses_training[-1], dice_similarities_training[-1])) + print('Validation Loss: {:.5f}, Validation Average Dice Similarity: {:.5f}'.format(get_average(losses_valid) ,get_average(dice_similarities_valid))) + + + end = time.time() + elapsed = end - start + print("Training & Validation Took " + str(elapsed/60) + " Minutes") - save_plot(train_losses, val_losses, "Loss", "LossCurve.png") - save_plot(train_dice, val_dice, "Dice Coefficient", "DiceCurve.png") + save_list_as_plot(trainList=losses_training, valList=losses_valid, type="Loss", path="LossCurve.png") + save_list_as_plot(trainList=dice_similarities_training, valList=dice_similarities_valid, type="Dice Coefficient", path="DiceCurve.png") -def train(loader, model, device, criterion, optimizer): +def train(dataLoader, model, device, criterion, optimizer, scheduler): model.train() - losses, dice_scores = [], [] - for images, labels in loader: - images, labels = images.to(device), labels.to(device) - outputs = model(images) + losses = list() + coefficients = list() + + for images, labels in dataLoader: + images = images.to(device) + labels = labels.to(device) + outputs = model(images) loss = criterion(outputs, labels) losses.append(loss.item()) - - dice_score = dice_coefficient(outputs, labels).item() - dice_scores.append(dice_score) + coefficients.append(dice_coefficient(outputs, labels).item()) optimizer.zero_grad() loss.backward() optimizer.step() + + scheduler.step() - return np.mean(losses), np.mean(dice_scores) + return get_average(losses), get_average(coefficients) +def validate(dataLoader, model, device, criterion, epochNumber): -def validate(loader, model, device, criterion): - model.eval() - losses, dice_scores = [], [] + losses = list() + coefficients = list() + model.eval() with torch.no_grad(): - for images, labels in loader: - images, labels = images.to(device), labels.to(device) - outputs = model(images) + for step, (images, labels) in enumerate(dataLoader): + images = images.to(device) + labels = labels.to(device) + outputs = model(images) loss = criterion(outputs, labels) losses.append(loss.item()) + coefficients.append(dice_coefficient(outputs, labels).item()) - dice_score = dice_coefficient(outputs, labels).item() - dice_scores.append(dice_score) + if (step == 0): + save_segments(images, labels, outputs, 9, epochNumber) + + return get_average(losses), get_average(coefficients) - return np.mean(losses), np.mean(dice_scores) +# Variable numList must be a list of number types only +def get_average(numList): + + size = len(numList) + count = 0 + for num in numList: + count += num + + return count / size def dice_loss(outputs, labels): + return 1 - dice_coefficient(outputs, labels) +def dice_coefficient(outputs, labels, epsilon=10**-8): -def dice_coefficient(outputs, labels, epsilon=1e-8): intersection = (outputs * labels).sum() - return (2. * intersection) / ((outputs + labels).sum() + epsilon) - + denom = (outputs + labels).sum() + epsilon + diceCoefficient = (2. * intersection) / denom + return diceCoefficient + +def print_model_info(model): + + print("Model No. of Parameters:", sum([param.nelement() for param in model.parameters()])) + print(model) + +def save_segments(images, labels, outputs, numComparisons, epochNumber=num_epochs, test=False): + + if numComparisons > batchSize: + numComparisons = batchSize + + images=images.cpu() + labels=labels.cpu() + outputs=outputs.cpu() + + fig, axs = plt.subplots(numComparisons, 3) + axs[0][0].set_title("Image") + axs[0][1].set_title("Ground Truth") + axs[0][2].set_title("Predicted") + for row in range(numComparisons): + img = images[row] + img = img.permute(1,2,0).numpy() + label = labels[row] + label = label.permute(1,2,0).numpy() + pred = outputs[row] + pred = pred.permute(1,2,0).numpy() + axs[row][0].imshow(img) + axs[row][0].xaxis.set_visible(False) + axs[row][0].yaxis.set_visible(False) + + axs[row][1].imshow(label, cmap="gray") + axs[row][1].xaxis.set_visible(False) + axs[row][1].yaxis.set_visible(False) + + axs[row][2].imshow(pred, cmap="gray") + axs[row][2].xaxis.set_visible(False) + axs[row][2].yaxis.set_visible(False) + + if (not test): + fig.suptitle("Validation Segments Epoch: " + str(epochNumber)) + #fig.tight_layout() + plt.savefig("ValidationSegmentsEpoch" + str(epochNumber)) + else: + fig.suptitle("Test Segments") + #fig.tight_layout() + plt.savefig("TestSegments") + plt.close() -def save_plot(train_list, val_list, metric, path): - epochs = list(range(1, num_epochs + 1)) - plt.plot(epochs, train_list, label=f"Training {metric}") - plt.plot(epochs, val_list, label=f"Validation {metric}") - plt.xlabel("Epochs") - plt.ylabel(metric) +def save_list_as_plot(trainList, valList, type, path): + + if (len(trainList) != len(valList)): + print("ERROR: Cannot display!") + + length = len(trainList) + xList = list() + x = 1 + for i in range(length): + xList.append(x) + x += 1 + + plt.xticks(np.arange(min(xList), max(xList)+1, 1.0)) + plt.plot(xList, trainList, label="Training " + type) + plt.plot(xList, valList, label="Validation " + type) plt.legend() - plt.title(f"Training and Validation {metric} Over Epochs") - plt.savefig(path) + plt.title("Training and Validation " + type + " Over Epochs") + plt.savefig(fname=path) plt.close() + + + if __name__ == "__main__": main() From ea218653ca5bebdff002c40f3f1d1d6cd933f46b Mon Sep 17 00:00:00 2001 From: Han1zen Date: Sun, 27 Oct 2024 22:11:30 +1000 Subject: [PATCH 10/26] Delete recognition/3D-UNT 48790835/utils.py --- recognition/3D-UNT 48790835/utils.py | 40 ---------------------------- 1 file changed, 40 deletions(-) delete mode 100644 recognition/3D-UNT 48790835/utils.py diff --git a/recognition/3D-UNT 48790835/utils.py b/recognition/3D-UNT 48790835/utils.py deleted file mode 100644 index ee9920980..000000000 --- a/recognition/3D-UNT 48790835/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -import torch -import torch.nn.functional as F - -class DiceLoss(torch.nn.Module): - def __init__(self, smooth=1e-5): - super(DiceLoss, self).__init__() - self.smooth = smooth - - def forward(self, outputs, targets): - num_classes = outputs.shape[1] - outputs = F.softmax(outputs, dim=1) - - # 将targets转换为one-hot编码 - targets_one_hot = F.one_hot(targets, num_classes=num_classes) - targets_one_hot = targets_one_hot.permute(0, 4, 1, 2, 3) # [B, C, D, H, W] - - outputs = outputs.contiguous().view(-1) - targets_one_hot = targets_one_hot.contiguous().view(-1) - - intersection = (outputs * targets_one_hot).sum() - dice = (2. * intersection + self.smooth) / (outputs.sum() + targets_one_hot.sum() + self.smooth) - loss = 1 - dice - - return loss - -def calculate_dice_coefficient(outputs, targets): - num_classes = outputs.shape[1] - outputs = F.softmax(outputs, dim=1) - preds = torch.argmax(outputs, dim=1) - - dice_score = 0.0 - for i in range(num_classes): - pred_i = (preds == i).float() - target_i = (targets == i).float() - intersection = (pred_i * target_i).sum() - union = pred_i.sum() + target_i.sum() - dice = (2. * intersection) / (union + 1e-5) - dice_score += dice - - return dice_score / num_classes \ No newline at end of file From 7edf621c2a6d341d91af5ed06c12444d0d77b2dd Mon Sep 17 00:00:00 2001 From: Han1zen Date: Sun, 27 Oct 2024 23:27:32 +1000 Subject: [PATCH 11/26] Add files via upload From 752ec48318f1421a2f820acf9d8b970e0fe486ee Mon Sep 17 00:00:00 2001 From: Han1zen Date: Sun, 27 Oct 2024 23:39:11 +1000 Subject: [PATCH 12/26] Add files via upload --- recognition/3D-UNT 48790835/dataset.py | 177 ++++++---- recognition/3D-UNT 48790835/modules.py | 214 +++++------ recognition/3D-UNT 48790835/predict.py | 127 +++---- recognition/3D-UNT 48790835/train.py | 334 ++++++------------ .../3D-UNT 48790835/train_loss_dice1.png | Bin 0 -> 49211 bytes 5 files changed, 350 insertions(+), 502 deletions(-) create mode 100644 recognition/3D-UNT 48790835/train_loss_dice1.png diff --git a/recognition/3D-UNT 48790835/dataset.py b/recognition/3D-UNT 48790835/dataset.py index ec625dcb2..076cea95d 100644 --- a/recognition/3D-UNT 48790835/dataset.py +++ b/recognition/3D-UNT 48790835/dataset.py @@ -1,80 +1,103 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import torchvision -import torchvision.transforms as transforms -from torch.utils.data import Dataset -from torchvision import datasets -from torchvision.transforms import ToTensor -from torchvision.io import read_image - -import time -import math -import matplotlib.pyplot as plt import os - - - -# create hyper paramaters -new_size = 128 - -# create dataset class to store all the images -class ISIC2018DataSet(Dataset): - def __init__(self, imgs_path, labels_path, transform=None, labelTransform=None): - self.LabelsPath = labels_path - self.ImagesPath = imgs_path - self.LabelNames = os.listdir(self.LabelsPath) - self.imageNames = os.listdir(self.ImagesPath) - self.LabelsSize = len(self.LabelNames) - self.ImagesSize = len(self.imageNames) - self.transform = transform - self.labelTransform = labelTransform +import torch +from torch.utils.data import Dataset, DataLoader +from monai.transforms import ( + Compose, + LoadImaged, + EnsureTyped, + RandFlipd, + Lambdad, + Resized, + EnsureChannelFirstd, + ScaleIntensityd, + RandRotate90d, + RandShiftIntensityd, +) + +# Transforms for training data: load, resize, and apply random flips and rotations +train_transforms = Compose([ + LoadImaged(keys=["image", "label"]), + EnsureChannelFirstd(keys=["image", "label"]), + ScaleIntensityd(keys="image"), # Normalize intensity + Lambdad(keys="image", func=lambda x: (x - x.min()) / (x.max() - x.min())), # Further normalization + RandRotate90d(keys=("image", "label"), prob=0.5), # Random 90-degree rotations + RandFlipd(keys=("image", "label"), prob=0.5, spatial_axis=[0]), + RandFlipd(keys=("image", "label"), prob=0.5, spatial_axis=[1]), + RandFlipd(keys=("image", "label"), prob=0.5, spatial_axis=[2]), + Resized(keys=["image", "label"], spatial_size=(256, 256, 128)), + EnsureTyped(keys=("image", "label"), dtype=torch.float32), +]) + +# Transforms for testing data: only load and normalize +val_transforms = Compose([ + LoadImaged(keys=["image", "label"]), + EnsureChannelFirstd(keys=["image", "label"]), + ScaleIntensityd(keys="image"), + Lambdad(keys="image", func=lambda x: (x - x.min()) / (x.max() - x.min())), + EnsureTyped(keys=("image", "label"), dtype=torch.float32), +]) + +class MRIDataset_pelvis(Dataset): + """ + Dataset class for reading pelvic MRI data. + """ + + def __init__(self, mode, dataset_path): + """ + Args: + mode (str): One of 'train', 'val', 'test'. + dataset_path (str): Root directory of the dataset. + """ + self.mode = mode + self.train_transform = train_transforms + self.test_transform = val_transforms + + # Load image and label file paths based on mode + if self.mode == 'train': + with open('train_list.txt', 'r') as f: + select_list = [_.strip() for _ in f.readlines()] + self.img_list = [os.path.join(dataset_path, 'semantic_MRs_anon', _) for _ in select_list] + self.label_list = [os.path.join(dataset_path, 'semantic_labels_anon', _.replace('_LFOV', '_SEMANTIC_LFOV')) + for _ in select_list] + + elif self.mode == 'test': + with open('test_list.txt', 'r') as f: + select_list = [_.strip() for _ in f.readlines()] + self.img_list = [os.path.join(dataset_path, 'semantic_MRs_anon', _) for _ in select_list] + self.label_list = [os.path.join(dataset_path, 'semantic_labels_anon', _.replace('_LFOV', '_SEMANTIC_LFOV')) + for _ in select_list] def __len__(self): - if self.ImagesSize != self.LabelsSize: - print("Bad Data! Please Check Data, or unpredictable behaviour!") - return -1 - else: - return self.ImagesSize - - def __getitem__(self, idx): - img_path = os.path.join(self.ImagesPath, self.imageNames[idx]) - - # This accounts for an invisible .db file in the test folder that can't be removed - if img_path == "isic_data/ISIC2018_Task1-2_Test_Input\Thumbs.db": - img_path = "isic_data/ISIC2018_Task1-2_Test_Input\ISIC_0036309.jpg" - self.imageNames[idx] = "ISIC_0036309.jpg" - image = read_image(img_path) - label_path = os.path.join(self.LabelsPath, self.imageNames[idx].removesuffix(".jpg") + "_segmentation.png") - label = read_image(label_path) - - if self.transform: - image = self.transform(image) - if self.labelTransform: - label = self.labelTransform(label) - - return image, label - - - - -# functions to transform the images -def img_transform(): - - img_tr = transforms.Compose([ - transforms.ToPILImage(), - transforms.ToTensor(), - transforms.Resize((new_size, new_size)) - ]) - - return img_tr - -def label_transform(): - - label_tr = transforms.Compose([ - transforms.ToPILImage(), - transforms.ToTensor(), - transforms.Resize((new_size, new_size)) - ]) - - return label_tr + return len(self.label_list) + + def __getitem__(self, index): + img_path = self.img_list[index] + label_path = self.label_list[index] + + if self.mode == 'train': + augmented = self.train_transform({'image': img_path, 'label': label_path}) + return augmented['image'], augmented['label'] + + if self.mode == 'test': + augmented = self.test_transform({'image': img_path, 'label': label_path}) + return augmented['image'], augmented['label'] + + +if __name__ == '__main__': + # Test the dataset + test_dataset = MRIDataset_pelvis(mode='test', dataset_path=r"path_to_your_dataset") + test_dataloader = DataLoader(dataset=test_dataset, batch_size=2, shuffle=False) + print(len(test_dataset)) + for batch_ndx, sample in enumerate(test_dataloader): + print('test') + print(sample[0].shape) + print(sample[1].shape) + break + + train_dataset = MRIDataset_pelvis(mode='train', dataset_path=r"path_to_your_dataset") + train_dataloader = DataLoader(dataset=train_dataset, batch_size=2, shuffle=False) + for batch_ndx, sample in enumerate(train_dataloader): + print('train') + print(sample[0].shape) + print(sample[1].shape) + break diff --git a/recognition/3D-UNT 48790835/modules.py b/recognition/3D-UNT 48790835/modules.py index bee5d9733..23c731959 100644 --- a/recognition/3D-UNT 48790835/modules.py +++ b/recognition/3D-UNT 48790835/modules.py @@ -1,147 +1,93 @@ -# source code of the module components, enhanced unet is 6 years old so there should be plenty of literature to build it - import torch import torch.nn as nn +import torch.nn.functional as F +from dataset import MRIDataset_pelvis +from torch.utils.data import DataLoader -# Hyper parameters -negativeSlope = 10**-2 -pDrop = 0.3 - -class Improved2DUnet(nn.Module): - def __init__(self, in_channels = 3, out_channels = 1, features = [16, 32, 64, 128, 256]): - - super(Improved2DUnet, self).__init__() - self.in_channels = in_channels - self.out_channels = out_channels - self.features = features - self.features_reversed = list(reversed(features)) - - self.lrelu = nn.LeakyReLU(negative_slope=negativeSlope) - self.dropout = nn.Dropout2d(p=pDrop) - self.upScale = nn.Upsample(scale_factor=2, mode='nearest') - self.softmax = nn.Softmax(dim=1) - - self.convs_context = nn.ModuleList() - self.contexts = nn.ModuleList() - self.norm_relus_context = nn.ModuleList() - self.convs_norm_relu_local = nn.ModuleList() - self.convs_local = nn.ModuleList() - self.upSamples = nn.ModuleList() - - for i in range(5): - if i == 0: - self.convs_context.append(nn.Conv2d(self.in_channels, self.features[i], kernel_size=3, stride=1, padding=1, bias=False)) - self.convs_local.append(nn.Conv2d(self.features_reversed[i + 1], self.features_reversed[i + 1], kernel_size=1, stride=1, padding=0, bias=False)) - elif i == 4: - self.convs_context.append(nn.Conv2d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) - self.convs_local.append(nn.Conv2d(self.features_reversed[i - 1], self.out_channels, kernel_size=1, stride=1, padding=0, bias=False)) - else: - self.convs_context.append(nn.Conv2d(self.features[i - 1], self.features[i], kernel_size=3, stride=2, padding=1, bias=False)) - self.convs_local.append(nn.Conv2d(self.features_reversed[i - 1], self.features_reversed[i], kernel_size=1, stride=1, padding=0, bias=False)) - - conv = self.norm_lrelu_conv(features[i], self.features[i]) - self.contexts.append(self.context(conv, conv)) - if i < 4: - norm_lrelu = self.norm_lrelu(self.features[i]) - self.norm_relus_context.append(norm_lrelu) - - for p in range(4): - self.convs_norm_relu_local.append(self.conv_norm_lrelu(self.features_reversed[p], self.features_reversed[p])) - self.upSamples.append(self.up_sample(self.features_reversed[p], self.features_reversed[p + 1])) - - self.norm_local0 = nn.InstanceNorm2d(self.features_reversed[1]) - self.deep_segment_2_conv = nn.Conv2d(self.features_reversed[1], self.out_channels, kernel_size=1, stride=1, padding=0, bias=False) - self.deep_segment_3_conv = nn.Conv2d(self.features_reversed[2], self.out_channels, kernel_size=1, stride=1, padding=0, bias=False) - - def up_sample(self, feat_in, feat_out): - return nn.Sequential( - nn.InstanceNorm2d(feat_in), - self.lrelu, - self.upScale, - nn.Conv2d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), - nn.InstanceNorm2d(feat_out), - self.lrelu - ) - def context(self, conv1, conv2): - return nn.Sequential( - conv1, - self.dropout, - conv2 - ) +class UNet3D(nn.Module): + """3D U-Net model for segmentation tasks.""" - def norm_lrelu_conv(self, feat_in, feat_out): - return nn.Sequential( - nn.Conv2d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), - nn.InstanceNorm2d(feat_out), - self.lrelu) + def __init__(self, in_channel=1, out_channel=6): + super(UNet3D, self).__init__() - def norm_lrelu(self, feat): + # Encoder layers with Batch Normalization + self.encoder1 = self.conv_block(in_channel, 32) + self.encoder2 = self.conv_block(32, 64) + self.encoder3 = self.conv_block(64, 128) + self.encoder4 = self.conv_block(128, 256) + + # Decoder layers with Dropout + self.decoder2 = self.deconv_block(256, 128) + self.decoder3 = self.deconv_block(128, 64) + self.decoder4 = self.deconv_block(64, 32) + self.decoder5 = nn.Conv3d(32, out_channel, kernel_size=1) # Output layer + + def conv_block(self, in_channels, out_channels): return nn.Sequential( - nn.InstanceNorm2d(feat), - self.lrelu + nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1), + nn.BatchNorm3d(out_channels), + nn.LeakyReLU(inplace=True) ) - def conv_norm_lrelu(self, feat_in, feat_out): + def deconv_block(self, in_channels, out_channels): return nn.Sequential( - nn.Conv2d(feat_in, feat_out, kernel_size=3, stride=1, padding=1, bias=False), - nn.InstanceNorm2d(feat_out), - nn.LeakyReLU()) - + nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1), + nn.BatchNorm3d(out_channels), + nn.LeakyReLU(inplace=True), + nn.Dropout(0.5) + ) + def forward(self, x): - residuals = dict() - skips = dict() - out = x - - #Contextualization level 1 to 5 - for i in range(5): - out = self.convs_context[i](out) - residuals[i] = out - out = self.contexts[i](out) - out += residuals[i] - if (i < 4): - out = self.norm_relus_context[i](out) - skips[i] = out - - - # localization level 1 - - out = self.upSamples[0](out) - out = self.convs_local[0](out) - out = self.norm_local0(out) - out = self.lrelu(out) - - # Localization level 2-5 - - for j in range(4): - out = torch.cat([out, skips[3-j]], dim=1) - out = self.convs_norm_relu_local[j](out) - if (j == 1): - ds2 = out - elif (j == 2): - ds3 = out - if (j == 3): - out = self.convs_local[j+1](out) - else: - out = self.convs_local[j+1](out) - if (j < 3): - out = self.upSamples[j+1](out) - - - #segment layer summation - - ds2_conv = self.deep_segment_2_conv(ds2) - ds2_conv_upscale = self.upScale(ds2_conv) - - ds3_conv = self.deep_segment_3_conv(ds3) - ds2_ds3_upscale = ds2_conv_upscale + ds3_conv - - ds2_ds3_upscale_upscale = self.upScale(ds2_ds3_upscale) - out += ds2_ds3_upscale_upscale - - #Sigmoid Layer instead of softmax, justified in above docstring. - - out= torch.sigmoid(out) - + # Encoder part + out1 = self.encoder1(x) + out2 = self.encoder2(F.max_pool3d(out1, kernel_size=2, stride=2)) + out3 = self.encoder3(F.max_pool3d(out2, kernel_size=2, stride=2)) + out4 = self.encoder4(F.max_pool3d(out3, kernel_size=2, stride=2)) + + # Decoder part + out = F.interpolate(self.decoder2(out4), scale_factor=(2, 2, 2), mode='trilinear') + out = out + out3 + out = F.interpolate(self.decoder3(out), scale_factor=(2, 2, 2), mode='trilinear') + out = out + out2 + + out = F.interpolate(self.decoder4(out), scale_factor=(2, 2, 2), mode='trilinear') + out = out + out1 + + out = self.decoder5(out) # No activation here, will apply softmax during evaluation return out + + +class DiceLoss(nn.Module): + """Dice loss for evaluating segmentation accuracy.""" + + def __init__(self, smooth=1): + super(DiceLoss, self).__init__() + self.smooth = smooth + + def forward(self, inputs, targets): + assert inputs.shape == targets.shape, f"Shapes don't match: {inputs.shape} != {targets.shape}" + inputs = inputs[:, 1:] # Skip background class + targets = targets[:, 1:] # Skip background class + axes = tuple(range(2, len(inputs.shape))) # Sum over elements per sample and per class + intersection = torch.sum(inputs * targets, axes) + addition = torch.sum(inputs ** 2 + targets ** 2, axes) + return 1 - torch.mean((2 * intersection + self.smooth) / (addition + self.smooth)) + + +if __name__ == '__main__': + # Testing the dataset and model + test_dataset = MRIDataset_pelvis(mode='test', dataset_path=r"path_to_your_dataset") + test_dataloader = DataLoader(dataset=test_dataset, batch_size=2, shuffle=False) + model = UNet3D(in_channel=1, out_channel=6) + + for batch_ndx, sample in enumerate(test_dataloader): + print('Test batch:') + print('Image shape:', sample[0].shape) + print('Label shape:', sample[1].shape) + output = model(sample[0]) + print('Output shape:', output.shape) + + labely = torch.nn.functional.one_hot(sample[1].squeeze(1).long(), num_classes=6).permute(0, 4, 1, 2, 3).float() + break diff --git a/recognition/3D-UNT 48790835/predict.py b/recognition/3D-UNT 48790835/predict.py index 597c0d76f..ce10f53ba 100644 --- a/recognition/3D-UNT 48790835/predict.py +++ b/recognition/3D-UNT 48790835/predict.py @@ -1,62 +1,69 @@ -import dataset -import modules -import train -import matplotlib.pyplot as plt -from torch.utils.data import DataLoader -from tqdm import tqdm - import torch +import numpy as np +import random +import argparse +from modules import UNet3D +from dataset import MRIDataset_pelvis +from torch.utils.data import DataLoader import torch.nn as nn -import torch.nn.functional as F -import torch.optim as optim -import torchvision -import torchvision.transforms as transforms -import time - -# Add your own paths here -testImagesPath = "isic_data/ISIC2018_Task1-2_Test_Input" -testLabelsPath = "isic_data/ISIC2018_Task1_Test_GroundTruth" - -def main(): - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - if not torch.cuda.is_available(): - print("Warning CUDA not Found. Using CPU") - - testDataSet = dataset.ISIC2017DataSet(testImagesPath, testLabelsPath, dataset.ISIC_transform_img(), dataset.ISIC_transform_label()) - testDataloader = DataLoader(testDataSet, batch_size=train.batchSize, shuffle=False) - - model = modules.Improved2DUnet() - model.load_state_dict(torch.load(train.modelPath)) - model.to(device) - print("Model Successfully Loaded") - - test(testDataloader, model, device) - -def test(dataLoader, model, device): - losses_validation = list() - dice_similarities_validation = list() - - print("> Test Inference Commenced") - start = time.time() - model.eval() - with torch.no_grad(): - print(dataLoader) - for step, (images, labels) in enumerate(dataLoader): - print(step) - images = images.to(device) - labels = labels.to(device) - - outputs = model(images) - losses_validation.append(train.dice_loss(outputs, labels)) - dice_similarities_validation.append(train.dice_coefficient(outputs, labels)) - - if (step == 0): - train.save_segments(images, labels, outputs, 9, test=True) - - print('Test Loss: {:.5f}, Test Average Dice Similarity: {:.5f}'.format(train.get_average(losses_validation) ,train.get_average(dice_similarities_validation))) - end = time.time() - elapsed = end - start - print("Test Inference took " + str(elapsed/60) + " mins in total") - -if __name__ == "__main__": - main() + +# Set random seed for reproducibility +seed = 42 +torch.manual_seed(seed) +np.random.seed(seed) +random.seed(seed) + +# Load the model +model = UNet3D(in_channel=1, out_channel=6).cuda() +model.load_state_dict(torch.load(r'epoch_19_lossdice1.pth')) +model.eval() + +# Define the test dataloader +test_dataset = MRIDataset_pelvis(mode='test', dataset_path=r'C:\Users\111\Desktop\3710\新建文件夹\数据集\Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-\data\HipMRI_study_complete_release_v1') +test_dataloader = DataLoader(dataset=test_dataset, batch_size=1, shuffle=False) + +# Define weighted Dice loss function +class WeightedDiceLoss(nn.Module): + def __init__(self, weights=None, smooth=1): + super(WeightedDiceLoss, self).__init__() + self.weights = weights + self.smooth = smooth + + def forward(self, inputs, targets): + # Flatten the input and target tensors + inputs = inputs.view(-1) + targets = targets.view(-1) + + intersection = (inputs * targets).sum() + total = inputs.sum() + targets.sum() + + # Calculate Dice coefficient + dice = (2. * intersection + self.smooth) / (total + self.smooth) + + if self.weights is not None: + return (1 - dice) * self.weights + return 1 - dice + +valid_loss = [] +for idx, (data_x, data_y) in enumerate(test_dataloader): + data_x = data_x.to(torch.float32).cuda() + data_y = data_y.to(torch.float32).cuda().squeeze() + + # Get model outputs + outputs = model(data_x) + + # Get the predicted class with the maximum value + outputs_class = torch.argmax(outputs, dim=1).squeeze() + + # Calculate the intersection with the ground truth + intersection = torch.sum(outputs_class == data_y) + assert outputs_class.size() == data_y.size() + + # Calculate the Dice coefficient + dice_coeff = intersection.item() / outputs_class.numel() + print('Dice Coefficient:', dice_coeff) + valid_loss.append(dice_coeff) + +# Print the average Dice coefficient for the test set +average_loss = np.average(valid_loss) +print('Average Dice Coefficient:', average_loss) diff --git a/recognition/3D-UNT 48790835/train.py b/recognition/3D-UNT 48790835/train.py index 6b1f7da01..e0649b6f7 100644 --- a/recognition/3D-UNT 48790835/train.py +++ b/recognition/3D-UNT 48790835/train.py @@ -1,240 +1,112 @@ -# source code for training, validating, testing and saving the model -import dataset -import modules -import matplotlib.pyplot as plt -from torch.utils.data import DataLoader -from tqdm import tqdm - import torch -import torch.nn as nn -import torch.nn.functional as F -import torch.optim as optim -import torchvision -import torchvision.transforms as transforms -import time import numpy as np - -# Hyper-parameters -num_epochs = 30 -learning_rate = 5 * 10**-4 -batchSize = 16 -learning_rate_decay = 0.985 - -# set up the funcitonality from the imported dataset.py and modules.py - -# Add your own paths here -validationImagesPath = "isic_data/ISIC2018_Task1-2_Validation_Input" -trainImagesPath = "isic_data/ISIC2018_Task1-2_Training_Input_x2" -validationLabelsPath = "isic_data/ISIC2018_Task1_Validation_GroundTruth" -trainLabelsPath = "isic_data/ISIC2018_Task1_Training_GroundTruth_x2" -modelPath = "model.pt" - -def init(): - validDataSet = dataset.ISIC2018DataSet(validationImagesPath, validationLabelsPath, dataset.img_transform(), dataset.label_transform()) - validDataloader = DataLoader(validDataSet, batch_size=batchSize, shuffle=False) - trainDataSet = dataset.ISIC2018DataSet(trainImagesPath, trainLabelsPath, dataset.img_transform(), dataset.label_transform()) - trainDataloader = DataLoader(trainDataSet, batch_size=batchSize, shuffle=True) - - # Device configuration - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - if not torch.cuda.is_available(): - print("Warning CUDA not Found. Using CPU") - - dataLoaders = dict() - dataLoaders["valid"] = validDataloader - dataLoaders["train"] = trainDataloader - - dataSets = dict() - dataSets["valid"] = validDataSet - dataSets["train"] = trainDataSet - - return dataSets, dataLoaders, device - -def main(): - dataSets, dataLoaders, device = init() - model = modules.Improved2DUnet() - model = model.to(device) - - # training and validating - train_and_validate(dataLoaders, model, device) - - # saving - torch.save(model.state_dict(), modelPath) - -def train_and_validate(dataLoaders, model, device): - # Define optimization parameters and loss according to Improved Unet Paper. - criterion = dice_loss - optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=10**-5) - scheduler = optim.lr_scheduler.ExponentialLR(optimizer=optimizer, gamma=learning_rate_decay) - - losses_training = list() - dice_similarities_training = list() - losses_valid = list() - dice_similarities_valid = list() - - print("Training and Validation Commenced:") - start = time.time() - epochNumber = 0 - - for epoch in range(num_epochs): - epochNumber += 1 - train_loss, train_coeff = train(dataLoaders["train"], model, device, criterion, optimizer, scheduler) - valid_loss, valid_coeff = validate(dataLoaders["valid"], model, device, criterion, epochNumber) - - losses_training.append(train_loss) - dice_similarities_training.append(train_coeff) - losses_valid.append(valid_loss) - dice_similarities_valid.append(valid_coeff) - - - print ("Epoch [{}/{}], Training Loss: {:.5f}, Training Dice Similarity {:.5f}".format(epoch+1, num_epochs, losses_training[-1], dice_similarities_training[-1])) - print('Validation Loss: {:.5f}, Validation Average Dice Similarity: {:.5f}'.format(get_average(losses_valid) ,get_average(dice_similarities_valid))) - - - end = time.time() - elapsed = end - start - print("Training & Validation Took " + str(elapsed/60) + " Minutes") - - save_list_as_plot(trainList=losses_training, valList=losses_valid, type="Loss", path="LossCurve.png") - save_list_as_plot(trainList=dice_similarities_training, valList=dice_similarities_valid, type="Dice Coefficient", path="DiceCurve.png") +import random +import argparse +from modules import UNet3D +from dataset import MRIDataset_pelvis +from torch.utils.data import DataLoader +import time +import matplotlib.pyplot as plt -def train(dataLoader, model, device, criterion, optimizer, scheduler): +## set random seed +seed = 42 +torch.manual_seed(seed) +np.random.seed(seed) +random.seed(seed) + +parser = argparse.ArgumentParser() +parser.add_argument('--lr',default=0.001) +parser.add_argument('--epoch',default=20) +parser.add_argument('--device',default='cuda') +parser.add_argument('--loss',default='dice') +parser.add_argument('--dataset_root', type=str, default=r'C:\Users\111\Desktop\3710\新建文件夹\数据集\Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-\data\HipMRI_study_complete_release_v1', help='Root directory of the dataset') +args = parser.parse_args() + +##define the model +model=UNet3D(in_channel=1, out_channel=6).cuda() + +class DiceLoss(torch.nn.Module): + def __init__(self, smooth=1): + super(DiceLoss, self).__init__() + self.smooth = smooth + + def forward(self, inputs, targets): + assert inputs.shape == targets.shape, f"Shapes don't match {inputs.shape} != {targets.shape}" + inputs = inputs[:,1:] # skip background class + targets = targets[:,1:] # skip background class + axes = tuple(range(2, len(inputs.shape))) # sum over elements per sample and per class + intersection = torch.sum(inputs * targets, axes) + addition = torch.sum(torch.square(inputs) + torch.square(targets), axes) + return 1 - torch.mean((2 * intersection + self.smooth) / (addition + self.smooth)) + +##define the loss +if args.loss =='mse': + criterion = torch.nn.MSELoss().cuda() +elif args.loss =='dice': + criterion = DiceLoss().cuda() +elif args.loss =='ce': + criterion = torch.nn.CrossEntropyLoss().cuda() +optimizer = torch.optim.Adam(model.parameters(),lr=args.lr) + +train_loss = [] +valid_loss = [] +train_epochs_loss = [] +valid_epochs_loss = [] + +##define the train-dataloader and test_dataloader +train_dataset = MRIDataset_pelvis(mode='train',dataset_path=args.dataset_root) +train_dataloader = DataLoader(dataset=train_dataset, batch_size=1, shuffle=True) +test_dataset = MRIDataset_pelvis(mode='test',dataset_path=args.dataset_root) +test_dataloader = DataLoader(dataset=test_dataset, batch_size=1, shuffle=False) + +##the training and valid process +start_time =time.time() +for epoch in range(args.epoch): model.train() + train_epoch_loss = [] - losses = list() - coefficients = list() - - for images, labels in dataLoader: - images = images.to(device) - labels = labels.to(device) - - outputs = model(images) - loss = criterion(outputs, labels) - losses.append(loss.item()) - coefficients.append(dice_coefficient(outputs, labels).item()) - + for idx,(data_x,data_y) in enumerate(train_dataloader): + data_x = data_x.to(torch.float32).cuda() + data_y = data_y.to(torch.float32).cuda() + labely=torch.nn.functional.one_hot(data_y.squeeze(1).long(),num_classes=6).permute(0,4,1,2,3).float() + outputs = model(data_x) optimizer.zero_grad() + loss = criterion(labely,outputs) loss.backward() optimizer.step() - - scheduler.step() - - return get_average(losses), get_average(coefficients) - -def validate(dataLoader, model, device, criterion, epochNumber): - - losses = list() - coefficients = list() - - model.eval() - with torch.no_grad(): - for step, (images, labels) in enumerate(dataLoader): - images = images.to(device) - labels = labels.to(device) - - outputs = model(images) - loss = criterion(outputs, labels) - losses.append(loss.item()) - coefficients.append(dice_coefficient(outputs, labels).item()) - - if (step == 0): - save_segments(images, labels, outputs, 9, epochNumber) - - return get_average(losses), get_average(coefficients) - - -# Variable numList must be a list of number types only -def get_average(numList): - - size = len(numList) - count = 0 - for num in numList: - count += num - - return count / size - -def dice_loss(outputs, labels): - - return 1 - dice_coefficient(outputs, labels) - -def dice_coefficient(outputs, labels, epsilon=10**-8): - - intersection = (outputs * labels).sum() - denom = (outputs + labels).sum() + epsilon - diceCoefficient = (2. * intersection) / denom - return diceCoefficient - -def print_model_info(model): - - print("Model No. of Parameters:", sum([param.nelement() for param in model.parameters()])) - print(model) - -def save_segments(images, labels, outputs, numComparisons, epochNumber=num_epochs, test=False): - - if numComparisons > batchSize: - numComparisons = batchSize - - images=images.cpu() - labels=labels.cpu() - outputs=outputs.cpu() - - fig, axs = plt.subplots(numComparisons, 3) - axs[0][0].set_title("Image") - axs[0][1].set_title("Ground Truth") - axs[0][2].set_title("Predicted") - for row in range(numComparisons): - img = images[row] - img = img.permute(1,2,0).numpy() - label = labels[row] - label = label.permute(1,2,0).numpy() - pred = outputs[row] - pred = pred.permute(1,2,0).numpy() - axs[row][0].imshow(img) - axs[row][0].xaxis.set_visible(False) - axs[row][0].yaxis.set_visible(False) - - axs[row][1].imshow(label, cmap="gray") - axs[row][1].xaxis.set_visible(False) - axs[row][1].yaxis.set_visible(False) - - axs[row][2].imshow(pred, cmap="gray") - axs[row][2].xaxis.set_visible(False) - axs[row][2].yaxis.set_visible(False) - - if (not test): - fig.suptitle("Validation Segments Epoch: " + str(epochNumber)) - #fig.tight_layout() - plt.savefig("ValidationSegmentsEpoch" + str(epochNumber)) - else: - fig.suptitle("Test Segments") - #fig.tight_layout() - plt.savefig("TestSegments") - plt.close() - -def save_list_as_plot(trainList, valList, type, path): - - if (len(trainList) != len(valList)): - print("ERROR: Cannot display!") - - length = len(trainList) - xList = list() - x = 1 - for i in range(length): - xList.append(x) - x += 1 - - plt.xticks(np.arange(min(xList), max(xList)+1, 1.0)) - plt.plot(xList, trainList, label="Training " + type) - plt.plot(xList, valList, label="Validation " + type) - plt.legend() - plt.title("Training and Validation " + type + " Over Epochs") - plt.savefig(fname=path) - plt.close() - - - - - -if __name__ == "__main__": - main() + train_epoch_loss.append(loss.item()) + train_loss.append(loss.item()) + if idx%(len(train_dataloader)//2)==0: + epoch_time=time.time()-start_time + print("epoch={}/{},{}/{}of train, loss={} epoch time{}".format( + epoch, args.epoch, idx, len(train_dataloader),loss.item(),epoch_time)) + train_epochs_loss.append(np.average(train_epoch_loss)) + epoch_time=time.time()-start_time + print(f'epoch{epoch}:',train_epochs_loss) + if epoch%1==0: + model.eval() + valid_epoch_loss = [] + for idx,(data_x,data_y) in enumerate(test_dataloader): + data_x = data_x.to(torch.float32).to(args.device) + data_y = data_y.to(torch.float32).to(args.device) + labely=torch.nn.functional.one_hot(data_y.squeeze(1).long(),num_classes=6).permute(0,4,1,2,3).float() + outputs = model(data_x) + loss = criterion(outputs,labely) + valid_epoch_loss.append(loss.item()) + valid_loss.append(loss.item()) + valid_epochs_loss.append(np.average(valid_epoch_loss)) + ##save the trained model + torch.save(model.state_dict(),f'epoch_{epoch}_loss{args.loss}1.pth') + +#plot the loss graph +plt.figure(figsize=(12,4)) +plt.subplot(121) +plt.plot(train_loss[:]) +plt.title(f"train_loss_{args.loss}") +plt.subplot(122) +plt.plot(train_epochs_loss[1:],'-o',label="train_loss") +plt.plot(valid_epochs_loss[1:],'-o',label="valid_loss") +plt.title("epochs_loss") +plt.legend() +plt.savefig(f"train_loss_{args.loss}1.png") \ No newline at end of file diff --git a/recognition/3D-UNT 48790835/train_loss_dice1.png b/recognition/3D-UNT 48790835/train_loss_dice1.png new file mode 100644 index 0000000000000000000000000000000000000000..2f2a6242c6ebc9e44d5a003450b3d15c9b3e8940 GIT binary patch literal 49211 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 literal 0 HcmV?d00001 From 4e3eb6848ffbd90d18f2a2bb0a376921f91076b8 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:04:52 +1000 Subject: [PATCH 13/26] Create 1 --- recognition/3D-UNT 48790835/picture/1 | 1 + 1 file changed, 1 insertion(+) create mode 100644 recognition/3D-UNT 48790835/picture/1 diff --git a/recognition/3D-UNT 48790835/picture/1 b/recognition/3D-UNT 48790835/picture/1 new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/recognition/3D-UNT 48790835/picture/1 @@ -0,0 +1 @@ + From 474bec4f7fe8c8ba3aef3590db02a055cdbd1377 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:05:22 +1000 Subject: [PATCH 14/26] Add files via upload --- .../3D-UNT 48790835/picture/3D U-Net.webp | Bin 0 -> 21464 bytes recognition/3D-UNT 48790835/picture/loss.jpg | Bin 0 -> 17862 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 recognition/3D-UNT 48790835/picture/3D U-Net.webp create mode 100644 recognition/3D-UNT 48790835/picture/loss.jpg diff --git a/recognition/3D-UNT 48790835/picture/3D U-Net.webp b/recognition/3D-UNT 48790835/picture/3D U-Net.webp new file mode 100644 index 0000000000000000000000000000000000000000..bfa47324f8bfb0add3207f2ba1c819f85d1d6ee0 GIT binary patch literal 21464 zcmagFW0+;nl097PvTfV8ZL7<+ZQHhO+vuw5vTfVu+gCI9{$~F3=5x-MbN0^2jK~$S zR_@%25~89rN&oxCkDy}q_hD21md-3 zO_d-fEGQtD)u{7>2{*O*?l=h+3tye?ShU)ByEdJgc31BKdblxkZs2SGI{d;K{*ZF} zzTs>0-R1?j?A_*T@eTOq`W!zoq>L&)sz2f@`96MMf9?J{ehl4IJN4D`J;j^k+wt}I zhP!p0O?`^x_nqh4cio>+1J*ePes$yV>r<+v_3aRr?5if!myRs=9}|o`gZfQ34_`dn3`2N$jTpQ#z={|Fgz8iF3gPUW7wI~#P7~Z*K3kY{5BX46@ z54;i3AeD8ae5Q#`!rBxIJ`A7S@q~q69_ZbkMC9SMQbM49KZ|`Xj|%>D{BeE}>~XAw z9zpZ}-_Pt;i2V6$k_qQDB6<#O)Y~TSe;5!MNJiVoYaD$eq(v_4Mf-~Jzh;T3M~O+& zg%r|)033N#C9`Cs|0rnknq7xd)&c_`TJnAd0D-woJ+!?@Rn2lu07l-yeYqHP$fZ9P znv#a(0&E5jZiK^|Xwt;~?|J;E8T8K_+4ZtW>G@eJUY4I%AvnG)@Uj1=H>OfMh3TLy z$`OD}pKhqd14$MphZYGO)FFAWcuLA2EY`Z=e?2eURxCSr9NqoQ}=o(5CwCj0;YveCF&4tPPB^MY@G-Qc*ZrM6{mx$jmWzR@(E#!Ui(| z`}cc;Dd~p!OyeK^6+7c3`U$z>5llI74`ZNK(W^i0nlXDDb>#29{Hx`^-)$%tlZT6x z`(=f`xp@j&bI~`RO!-7!JBa1LN2{^$Aob&atbl*_962%4@BA8<&Wc>5y%a%&>uflF z&VA8c3RQC~cBxL`#S8CP^3Rg=ukjb1(b#2e+~-vh{&2G;{42C z&OLEpM31%7bh^ZGfaL*u;d4&`JLs(E5Q-BlwM_lplY6(SM>GOH>rcyJcAS{Ca`=D3 z>Mjp2uvqM}J;9gtnk%ePpqz$^DYP_NwKG4}eq5w}H1e-u{ue}7=J(qrM=Q7?e(#Gh zW^dgXy8dbTl?4BRpua>t0&FayyN7{yVDoSBZb->pEggDdWo0ienp^u9#{WISWxapz z0FFUv%dE7jKJ(VZ#KhWs6N{iV2jt-3;I;_xjRE=p4J;`C;3}uNw!1BOgRC(5k?)Rf zh7d#61vSUh{YGoXP5UJ$ZkPxSN$UxD?mr7~bMxfm>C{HtSNMM!!hc~Vy1-pZ<&BUm z>s$(EUb*yFiVYiOA0%hPOM3S4CB9Ds!ejkT3&($59`--|=e%Y1_Y3s1bQ>%`)O3Q% zGu_edJ2<6kk^dKW|MW!PW&`MF*&OpXXXb+`X_F_#{NAnZTj7 zNF{!Gr6!KWmX6SxMj`K9*ROK^CsqH&hZk(TNf=&c`J2E@4Wd0XD=L2U0bdC+VL+O0 ze@pCtt_BXRH(@cQ-owkAQiSl2_^!l*>G~f<0k)A1W27EG%CiX@HMIs(99d0!HpFt9 zI2JT@aNfDeTV+g32x7SO1aUOT0JOACUh}fh!dE4t7U;9K1`3o^hL-#vv8iRu?lt_= zqB4FEt{nVjtSHuJkRsAxE9`=BUW>}&iNF^sc=znUnr z#0Dam;8DWchhm6vo!`$itpeQVf!>l892tT%m1Aw z{zq0LArW?$h`I9^}!k%HELEBt?t5ROykcu+!rtdo4n#~X`0ETv*FX~Ar{uoeeE zXdgn5k(1a277f3SjReL&xJ3SRQ?lNP5Ph~)@OR%2@inMNP5u?49qM$hc$HHRuE^W+ z2L0{-g=v&!b)yxxj~&v0{Ux_lnI_D$66KIyQbn=WBc=e)HTB`)8Xz$)UeIu^&GI0% z{LK*;SWQy>BNkf43%=!$pAYbYU(=|hhLhVAcncZ`@34krz=_5{VrFdr&TIZQyMzyhiNT2BndD7kpgrKf^9McFXhpb5EZ^%kcJ4DD+>r^9 zU-hlWUQT)Rd6A=Ol@yNX4!F`BmSs_0O-C<-nH#7`sKh_2z?}sz*AkD#fSoPIYE!>p*)~K7T+iB4PLa5s8lke3@AR?T9Wh_6C z>T$ytxV0G9PY%FO3H6`U>q8ro*02y$B`0C>>QFWs)|$%=;v-cUEAzrVN9WGQqKXgw z5^@LnQ|7@B4HFDLH#zu3aD z7VoMJ!v}u=dZnYtDeKs4GZlHm>S!_Dn!q*PVfFnNuO#=>{qX~4#^7B}t31;LP+C6E+QjF9Ar95+kzU zcl0l}TAv;&=X=Zm!X(ZYA89Qe)hPU1Cgo()AG!UA){lb8)Z7X|QQNl>W zc;uJ?sJ}9W|5T*?a|E-^vz!9X2Iw9tIK!xZr1dN(Cch^hfMbn?otmyeQ~i6if60qWDYD0;R+YD0%Dz<|Q#h#iHaFeP z!GW(UT(oDqo8HdQ;Li!AT$FgHyMaR0QH*c+!1{aa%3wvkwOVpw#HV%2J3Q~9<(YtR+?&3 z7*Vo;t=3E)hAM-E>ZZ|~4#`J0YL=6!|-)D%CkcSWMtYVB?QYx2HitE2m>_dnC%D(C_TyH}_^skjryBB75gO2{TaM9MhQNP8^AW#t`Jx zB*vbwMimtX-I%rHBghH1;O&)v5mxsN1J;ht8$C3agxbH6aq|(Bv+rZs>qxT<2~nztbo zcofIeEJOfc972ou9%Uf%)x_v#3bdfQQ-9?Ru(s7=$`p9nJga^m#9;`(onLTKgROU^ z!K{O=qK+L>CCYV_YqvoSVjp+r@_dl?@WPDBkfq)m%86z20}9Mcy3fdy&!_|pMb20s z{|2T$^bZT>3t1sOp?~&{OK53gSqli*Sc7;Xj_JO)YpZhWwA?-!CJE@_U|%IM1!M0( z(MJP?x6S5;J!O`H-H)?fXblz3)KU2PFqAHlIBK>@JXp;8&e0{Z{Z%TgZ`mQG`|ZrI z(`&ER6Y=YQ)+7gNHhDQ?A*UGC$KCYRq+goAaPUZ|n%cj@gqD=@GdFZ)OPoDqqXdEq z)QzV>xBuI6@R?UxLKf-8gKuuoQ1C1CjKR*u8nfBUmaB=Eo8q_pfB?%iW8AL^H6nYW z-2}GnIX8+TX%7kLjAFP>=VT(s88}4VgJHrhrDBta;|_@Kt-ZG>WNP#4o_BqGS8=Yo z?kqv2A+&k9iftlc+mYfhrykH)l-yLnIL_J4?yr3DCZajS&dlt1*QJ_;AK1gB$-e01=)tjpz% z>KtAT(-D!PA8l1z%^zrrGDHh{>lS?zZNI?F9;UB9CUmj|ovt}DGQT+^jY&j>AByc_ zLCJ5tW%RzJpU)I@xgiohqhD=+K^Z)9c%5u~U+4oD6iNafz8AJp)6|H>FXSv%IY$?T zh#osaIMTa;hS4cfDTS8^LL96THbd?_Dw*|R`yX|VRoB`ZDFLP-kO{VAm+7eI4D&vI z@9qTtNV120i5?vDYn}uzdMZEs`BXqcwxCqtItLT zEA@shxQxFqnHlXLh>mrnG;;9=S^xmhIm$yOPHbqBL!k1D?%bO5x2ao4KehZ_DNLBY_@%6s}?n4D*cKganY=Uf-S+J z6_ex-D`L!%C`+b&5~&n0U(TEgJSHMkfJ+Tcsoj)9pk@gflyBmhcn;O^Jvllek^If> z0(F<7Pg}81@Ycp|gl2lx;kKVxCs2<_<%8H-vX~3dz)tA$vc?!o;Zw*Fb@Zjn_mwTK z25jal#?Q?^5NdewSvh*iQlNr`ZsHN?%T-rbEh$t=(eJwM$=AcMM@3Gd(>>$uSk|%! zB1yI;4olzt$06i4Wy}&CzNmxplMb#Iu%`UW<*l{CTAh6QIMQ8df zz7VsZmcZn0jHTYuIp4F$(Wv~xOPY&mh8#mUtg-js3Y3p&4spRd6NUOfmC?KC z|4d}WY|@rNRPr7ADp{_`Tuf93Tc5o=4jAM1Hlgf00PZD@Y}SG z(do?=B(7#w+L*nVcZE2X<^*1*0>R=|VsXq+JGDyH{w(M*)SdSVwPLh9?a+Z*VEUKA z9O3z9!dFwE!=3rm;ex-2#rb0Bo zka$OQaM`TGn>t??Q!~S)};*owtT>n>8j(%u8S5r zkV(LY%qt@G7=y|d`ZP#a6yr8TE`Vb8*r`ktM?8DPsO zaCnpjbA%YR>2o+d@03})(b;#9_eq*ChD7~_SyJ**Lz~o55NOqdDnqw5AaC&6ZftkC zuNEva{-fcL!MT$k57L$x7YZG{bJ&o27o$jhWhr=Z1Uj3Rn%I7lX#j zw!j5S+@Y`0sEjwDazhueCdL4%om#`t0g#>%W_d0@^D(T1>@j}JX|Ui&F{L|GA(yHP2g zob9OuSK<8eux3TPrk}B`O{~y`SJeoZgy6cqoi#c_>Klhu@;md>RDVfT2VetF;fH6-Vf9c4>LAlmTl(~TBS9|%%bKrkrJU}AFV23E#oJt~Cy#+cy{BZdnyIUuF9!}MP^}{q2#3R2elOMy z1f@&XfY?c8h1Zr{7O|srN8yNkngO>g3(t>q4wOp-oH0sZ9&(<0nvU%uV~5&qA){TO zj?npMsW0cD%7h2{@~Lw{cvD?cLq#6?#a;89r#s#wrNI z>{WqYf45<+XF15|dBd?FosHJ1h0DoJmLuHIvxU_YG|o8tk1vSkL77#yj+90u(ZDe7 ztTy~6!~2jad_j4k&wW!Bmyk&UJOyJ57w{}aWYg581){nqGbYuzYT^-ii2eXz%qE zemLUYOFs=n&N!qK^2k?^<+}$Ed%|ds2f1%0Pq#Nm75rNmnP@V28QtIL)~ z8lndP0HpDYO7>E@T+#=&R*NtuG)H~`KFHU7Nr7$KTe@mz=MR0gA>TI4Bg+Idus)}? z(LFKaZ=O6|!U%dJ`6ea6&?#u6wB4R@D2HP0SA?=ofbm6XwerI0e)*G~fu&6gXoWAQ zR_8x+MU2zC70)qcI1#S-9A!jGtAws4-MDoCmg*|ijnOM!UtDsqW4Ro1vzU^wDEMdqvllcrw@8!OPcdEX~KOEN8Fsi3%VYH&6>LCq!25kHsYY8~}YlSYA03d@3pQ{gWMJS78*}=l? z=uL*mgzQ(7#iWaj!RX)?Q=Xw_p@LoU(|vgEW1#jk?(5RlKSu*C-DXEw3qC7I$BG)> z?ejU|tCZ4e1T21w9I*Alc-zCHIL@(R4&6GU6?LuPJc<85m%w0jd^l$s4cqoI&SohRo5j zH+dzXfX}3*gP$zkyb~u7Df7opQpk(AUQ+Q%_;8v?*F$Vb!K4*?1Okz>N`|C~Ld_MO zkPSedGTpg-8+UVmL*^BqkWYX#C_sG!zgOfp63#x_8`lb7FJK=4uy^0I)UQ_{nj#cf zxr1SF$C7U}6+Z{()-T8Q93SUn`mi8#X+}nXueV6Ynj}46)I-0AEbH$zbBA0~9`?&~ zQ#S~yFJj>(vbPKcpHA|LNzmW-OY!b>b$8e>zmTk6AtFm$#ik*hQFMt7{9xNds}d1v zg{8sh7fWJ>QQ-HYr@S|nC=duys18r8#N{GFlOgH!?JTLs{D zQ)S9aNJP+==fsO2mz=bB-XPv?$-JB8#he_*+|`~(g3@L-tTGl!X*u4)eky! zfaxm^B}O^4i=R6L0B!s&*aFWy$CE4dXZ5YBF4g>st&;B&U#HJtjE_W=b$=8_8S8dr z2XTie@=`0Mkmm9T24BxR;rp6NpHjceHoU8e&A6sV1(&&$?&LLb<|mp_U=2p`@KbpC z6-IfLz$`?doDFX3F3!RAOYWmn!k{KZH^jh`{@BGJ30dt`SU;jE)}ECj7m)kP;cFUq zWmth*V|DI=Rrhbpm=<)&ht16!0Fp@Omk8WTeh-#Pv>c(Bak??Ty26i2-Y zXJ=J&vZYb%L;Z{q+<_qw<~bK~x*(lxkGi|&FIhp#%#)C&Cpq#$6>p3($f~lAt%`f2 zT6{YJQ?3dsUb!cE%PW9Yxf8=B`PL+6@I7W#55#A;BRW^BL(E)uXnnTFJA`UHT6)8Z zfococaKY+($dis$0ek-D2S(cQMmNL2jX$AXP*#elfn zq;-VDWQYUricD=Nn(P7uWe_C|g&8$BD~;beFPwhjuZ$27!V8Aa6aGSMk56+nZpRaW zk%X$;lPyFood$mJuWI6N*bbI6^tApw?Z~KwRVfF3p0l~q=x$xzKKID--K`D$LWR;} zj>Yc;0`%}7hmjjqy&%7M+|G%hRz2${kJ?hYB19H}jk;wJkm$XKL}?*f>&}}4@x`21 zY*k3SQBRY#DmwE2O)biJNuZw7Qo!O#5F(#*q`D}c;K1=owgzh8c<~#1pyaCu6SXH~ z+ABLKN_Hs*QA^rl4GdZ-Qfi)FtxFts^wRI6%XcTrEH z@8;95;8wf(ad4-O*s2)C8K6x=$Bg2>G$%Q2E-Vhw`{AA}Y1gQ1w~;#XF5KmKPnzU* zD;hb$(1w}HVjvZG8%-#sglMz1S1;Fi)8*ND6K;qN4dig^SU5b=er78K+%IH#o7Y*Ipo& zee?G0bi|G$mP{?h9n|&I1Ee&;F)I6gt^&l-@G%&!`aY^%aEqm#!H*w*!SYm3;QgaU znf(C^mAYqu(GD-JVEh1{77i2M#$Sv=2=|^Z(v8y=v+k4^t>uHBn1+pw-@(j9Tu_V) z-Pu&#AH($gKydnXafpUSP#Qedr)qDfoRhXrBT2v@({+Eo{md{9X7(3UpK&PwQ1lhW zggO0c91V+d+b~ovyGA2MmfYm2hz6{^9mKTt?#1q!NB_qY=RA?X6F$5?($&mW#SL~dQHjQQ%@G@~`fZ*hKw&%GXa6pSMPrY1!J z-pm-fNetxkgcOO3lwWLcCEvPKa+ejWpMBPSby#tQhaH7uKfnA-H4rV6uH3dxAIoXLOC5KIEG@fdrJ1|GGcSv`>#h~YiBVDPrD*7@y6>d zdSb?h%@`0xm1Gyb#KzfuAR?C4bVu@MD<=5eWCo!DTH)t?!AIHQ&&z3PZpe;m3#SDpQFmJCKZBFR}9aq1j4L`#5CB%$m z^#>Y)!?!@()0^)%vAZb2gL{2PGvJSJJO^d+7N*Tv3iX>W!z5IGy;F7oh~JP2=OY!X z_;?fwOhg^Xyw&tu=N*_Zc7c^n_2Pfx1NIJ3Zv1eqd?XnSUlZC08!+oF&e?DDTDZ3@ z_Oo$Wt%#cKx}`HINcn{6sAgCKlKdffgLmj9+qM(l3W|&z>C#Y`t0vAlZ~r~-4!HvS zh;sQt*iDjx=3$5(C}JX13QAG0E}R_ny-PXx%VeDB$hqxs(9Q^!2BuXSUzbZw>U-&j zAB!tGK+t0WI8%2FMR+JRo#bVSns{kReMp*1009i=41Z}F;V8a}|M=-pB*^ zcSPpIrI6zUwosi$rZP^j`DR9vy7!(9Kc39W&IxD90y6d28ZD04aRB#H58T|jK&8;x zbvEb^y?*wTz#okr7*fH9Aztg(R2%7!A@dJXj=zgQ5d6yY^kMDcPD4Cq7#;g zkzeL&wx2%Js`{7RkWw!P(Cr1q=EL#4Z^i}Jm*fVAkNHo-lxF7ple5*O5R*i9Wm}CvzN?{{60F%;DKs*jn(?{Rs6U& z6GSAdHpcI}l~UpEoJa6UP(*kUBgvcHbso!Jp5Q$TKXPW^?Uv~hdW&6|=`eS}!_+Q} z!dJ0lx!iOPBi2-l`RfPaxCSKZ5bQ~?l}Hn?!-w=!Jg^Fs&X4R-%=Qj~+>c-1VxS{I z&Nx185J1es*;I_b00NvxkNCP0r08FhS#}{yAGcieOX42O(E>mMdT(Q+4qm1`?$|cr z!qv1X@FikTcI@{VVzdWIgdQ0BM$Ip_h%KKuw~Viwja5h3+OyQ6Gw_*)jnlTPQcRhn z%Ro|%!86)Uk95biuPf0p$v8wJWC@=nhL|h#!HPXPqgNO5DD}xs>H>@G@Kp84`cT;0 z$LAa2CuL#HoS&HN@XJ4iTKDh!MyXHn-^f|a{0IFFWcB`>BStl&mV8Tgx)TYP9wz1i_@2Fd>t$nL|#bPnM7WR=HulL3sg_s+4SP$4j6v=faBB9jr(Ndeo zq`O-hu!(dJVu-N8by4i6Lu|3xsf7#Ib3AFBo;R@d84|OxaAWPhdw&0gGa>ntmLCA3 zVG=Yt*zi~k@gC8Dkp>f7ycE~1Akex7Z;D7KMnn#4@2#3u}NIrVkx&@gS__ z|E;x#b(4OOY<> zbAW1cihR}+70s&+nLU#3C;*cR&tXD@?1iyX!Yh60(fHdUIsib`A7HQ9bl>pD-r)~9 z&5v0fc!Zat!|h+Xce@Ioq)KvXDp5fPS(>$p@~s!T7Lld(i#3*hcXOzVmPNO0?1w^8LY0AqA+}@@sSmwniiGvyVz!3ib8k1Y zmG~l|KMykYKW7}p%7t6mfK8hUuM)XR*7-gNau|} zAP{LhL|%L<)rI}p9?`gj-Ej@}lH9!rFG%ljyh|$$EL1BW1HjR8BBZZ6gP1-5{U#gC z=ATo-Wix;^S2X--eq!NKnN(O)G|s^kuwkFuF2w(b4gwQ_ByngpqQO;Gtw9GDH}a!6 zs1GLzUA>&xA!V4i&7=*R^a;ZucL0i*OYS?QQZYE_U)g ziRip`rLW2sR|m~~C!?i8_zXT%VE^>^Hfex5M_Y~{B{YNK$WPNV&!^z1lhyXtFd|7= zNKTNrv!OCcRIsx#-QL@o*nfD(MPpiKueq*4wAF79onh3r|bvkVVHdc)d>#6mr2@QUOGH%w(p$m_4r>SC zW$a&`m5Y~T|8_ddP6gVt+d{DDD-I{H5GzaytXi>9g#aP@K2J!LT+wXiQ4o8OptqNA zCO-0-fcLIPnC7h)xlaWo;yLn>>ch#{7D&W9O!OO5i9jXGayn0U_8>G-b+=gC7iTg5 zIBviE%(S+H*>ycM?GiMsNesTIIjR+M+}Hvte)LLSM8zeS)=;bQDf>6uQ)ok|EO`;d z)f})SYVs`8B2;L0J!N}YHWib^Ye9->)C{jh)EVtepKOi!Sd{#Ho!~Lr?reO@rA_3R zSCuSrCzV#OMK0d)epU>5)@8-o=&Wm&C`2lP{FptTp=nq(q;7~WnF~(sA#FW@3?vPo zWVDkMu+&Z#vL|~)nu*Kyf?3>|>5b=Xp49T=)ra*919l&ETDyTj{Dk91=DS+ou>>5c zV(XbUSNl*9?-p1ErQU5RmuY#mAAm$zDP6nVy`!;v&Pj-g{eY!DEj^PN8^W+)z2QDv1xnKRY~_%2)%mRjw5aHEPSN^p1o5zY-;V2gnNc}laXsF% zUo#U|<49&ZW4Rf&6liotMk=3#CSG93921|tCG^!PCo!GdOhv<4GD|EA!b$%rU8OSk zk-o5nJ{BX;#2M@W=Vki<*9yQ2xRLA;;fzhYeqQ_TDfxB*+bPHk!j?;<^}5XcS?6-X zrm$~Bb)n5?zP$&=YN@q02f z10mu+;reNiNJ@?Fnee_ntIn79(|gyfxmW$=Am(C}C2FK&rv)g{0-pszlE?8><%Vby zW-iVhueOIC)sdpA^@A!tCx}9=6bGCVh z)2hL~EuS@zbvI%V(X?!mGs@$e=Mnf>dJoOXxS03Ce8B7~vTkT>zuI{tfUlsoHoaHG z3w|<>$Ld_Q{&|&(Cn==)8wg`~7xpl8Tkx}#bFs=J$Q|^Y~TxC$f3mt%!e-$e3J26Pdz=W<+#S-3h(f5vht3 z`F;St5UaG04WYKmFxoWD7?7b5DXhz%=SHl2x|IF>Y9dMd&w#?0mZ@c4Kh{W=pap+R zJX5AxrHVkEXQ3^`0a$QY)uecT8df!zC*Z-pEhzh8y`fV(HSE9=j>D?aH>*_Y1d)}Ziwc9&La*cFN=u&(_yBck8H<}x}GjYE~Ej8=j{Iu6k{8w<8vCw&B=5tm2PriH0QNIa+ znRe<)>+iNI$89~Rqi9Zj3vc5rQD%ywSu5_Ir#P{NZeAId&=gNR)TsC2z^KnoU{+6JT` zRPInMt!$m>E3q0Q)p+>gy<5`wNZ z?)^CfN3m*9J-@(&7Gzwa(JBL^=v5Poxg>AqPs+_YHulr*sSPB))256`Ix) z-Z%IAsD*`kA);#l^}F_hLu!DLS86&-WZ_FF27a{8AA}+QiEF!XG1wdGyKuIi6OePf zXp6DOlosoO9f^GU00A^|UfIFsC1{oF(J<>2m83Ov-c2C^BGVG43?!CLfPijCdkbC? z^C_%MvQE03U)!W7|3+yKdEiDym@CJ~(8V64p@m}jekGkKwy$8ZC>HRCYyB_uScEe_ zQL&T6V%_{Ue(3!$a6-tMio&1Y>y#|}`j6m&wNjq^T3-74n~CTEgqmb4zhsh42tS&@ z03zkkz{gNqYpVyr2)C-%VG%gPnem4_Zhq7z+=K?76AzGu!T)kGghe&)NDXp@4u>3v zYx#*?@5;tbqFMfO!;A54$42#|f|xdnJPop;afT$kImavJqg&cj3`J-?lFr!0Z!^N{ z9hTO14{}ND!0I~z(ys6tlV#r*&S_~p!gFa|W5#EevB+1!t!|$8yapIRVqCkB{SVbF-K}N7}#}HSe1^;;5 zKgB_e%k!8LuDhNo81~&bT);6gN=QsmA4Fv70_a(&A2AengSn~2>uMn;d^{6-YBBGW z)Yfs1f+|{=-o3h2He{!xNZHAm~8944Ip}Q z20JlAQ_)t<_a@X~jOIIJ%Rv&)$@^s4+8Z|MGh&w?zjhB$ir7WVQi@lg5vo{NEzBSjVC^9hO$W;sj2?M2FNp`}Z&+_$xi{Rz zRXRlAXMRj+g>ifQJNe^$Oq0%3;ceHl*JqCj`a1AKUELt*u2*X_ek2$#XeUdPtp2zv zEioqUO|Oh&YI#@q+4c|X4xyiOEAY8_I!QFqeTSPpMru$9*v)5xt>~+sQBQl!U)gW? z66#nY{Li^wYo*j1gpPK(oEGNGx}xJ_RTx_ba*Z7v`neCHp_>BH{t@uF+k!3DPVBp9 z<{m}>RQqj#B#S)YWRH)zGMTkB@@KOoGq#XK+coNCmug%0li~pax+V}x)K>GcP?sriFvRTDemj=5fcQQ+E!f9+{((KZ`7!F#--=@Wi27DO zmMX25=GdCphRsF_*X}7)a_#Cq0&6QhsQ`^peT`an0&mR`h3U*sW-HAF-Nh!M)A>i4@ z@aw*M5KlL~azo|%Xr8FL!Iur(%=KgX#5Tv1jGUx0#&Ng#l4=jZ`<-JLcrA+D&+mJa zi`c~r=`;@|RX})yJD~`$tId-@6s*Ey$I^E*&%o$xn<#~LMG1r|lrO%6?x!j9J@)j5 zr;8JFqne|rGP?0tT3IVLS!i{=`bKnlem3GwWLfkBymSr#1dQfn{gfqQMt9^NRD_)3 z>b#YMd>VtTV&~Qa$6s$=8w}F(qedUzlBK_BWW491cxQkuEWiEbb|9WOB?!uM&yB_| z6)!H@7y^N=RJcu-zhLf`(Z%aBtB1|y51|?=X37Aa-pT)g4tn7o2#15Mvj)%_I>~Hi zNf?)>O?3dlkvjN=uk;qp!FRq$n^7tUA_^xvW%fd+Oli8dR634al^>c}Q+bw3``|3Y zvOhi`DI}%vW`1*3`0`G*#^Kx);f{ab`||CnocQ9+?D?Q$OVh05MCfe#c&bi1zbgtW z78L;#60851rq%l!Ayk^r_yA)#53y50k57DhV<{avmOhCa=9!WA$CLPO)Lra^A-dB< zSb$pCwYIGGQI-ow^v|=@fr>U)l8*bAgn{fyzqTICekEF7)F=c@fLgpU)x`-$3r&ly z13jD(B3s2`Wrk)jNffF%c!Ojtq$T+U6U38w$d&*cW4U(2`EqWv>Fb=iHzG$U;oK&%6 z@A0F1esw)zdE5_4o`o9~qp+9!&o+~h<^r#=`e6Fw;p;1CXt{jejr;X)vKuybkUafnd z3}xg-Hls;yJ)8PX+P9ErU`@AZ$tyXDLrVDl5%+Z9rA9uQpXbeRJ&GH-;aaA3vIMDx z@z1Y%$#wIs&ExV}BaAkxS9|w3n*El^QomDdbVXiM`{;$JbgUIP->p3pyaUU?(uK=J z=_K#lt8vo2(l$iA@mNOQ8hapW?pn{r#NUA8I6&f}u-Jp32__Kej6~Ecd z25yo}{%Y*#sRpqP#ZRWzRXnJ?e*&AU z1m_dJT;!uv)BbY;So<{QUgq0=xQ!zbh5Mt@>q!0-=*6Kp?wz;&df*F@Fslid2sDMp z|3a$6h~){(GO(L$ZFQ~ml6BPzn{==BgyZ!o8a&aW2+D-VJiMqNc5~i4<7n^|49`k= zCMS<22!aoWB0M5SD~1Z;k*c&2ny}{VYl`{fr4}JG$i#@_{PNh5>UBHJHPZ7Ic9DZ(~J*^ciyt z=GftU~TPeUdfCS%ePYK$f?>z8wpoVqhmhDTJ&5R+-Y>>pLE>SEL*DhkBl`$=`5&w;$_V-XX?Q)y7F` z=Qs>ce9qvjL%e8?oux8&v9r#ZJ`Qd{@5zi$3+h!4otB?ui2USIu;>6GP^>(VH`(Cs z!lQuB%RHECbH`xxtAM&cW+XWbtr%PxlkkD~^FYU$oU;$MWrSD|5o*qEX#7;4yPM(+ zOZILz)OQi(AW1ZdL%$5hDBg2Oox^%aS&H?*YDsU2=$6lIFHmf%untyTnyqnzia|nJ zaPHI&gy;0Q#4d{|v~VgbyHPDk^s41s4(y`l_dio*woL8$p5qYusma~=)eiZX&N`I` zYX}JUPfZUat!=ab)%>)YJPn3Oyv_wrw6O0p`u8}=@NmMJS3y%wVqP3*k+9iPZHy%2 zDc^)zEGMUS%+${HGFrjIcuOZ4mB&oPZZCM&xbDYt>br-#)|K-L-wW6fk>9BF4C^rE zES3x2$-Rv)qkyy-e>dSdQCab_YdU&xKvE^q?yDHQYKH=_LA*t?0TgT zEo;t{`To;4=j{Aa}-y38eD%t4q1 zBsP4X#dLadb)w17UybOCD0r%mQumlUWYqs(5my}*1@pC+MvxArkzEl4R$3YaY3Wpu zZh=LbrCS=L*+rICDd`UBl{9N=nMg%HqgAlhPaK=5swJE?FqY@2bB4ul)e2$^Gb4 zk;|~egPh5s*W@3QuF>O|U-Wn;wZ9oUUhPf0@4B#-$0a52&vD9qrLQ;)G`CF9KPr%O z*rW}6bpXUl{)}U!-6`sh?Z0h#eUfK{i@)NkEn*7-nIo{K*LmORmMpIa85TY* zPjJs?kxYo3-&Hf4kedz3c8>@maVRSl;7D-l6_P)!P&Nc8=~-j^d}m%&jjIK;@g|SC zg#5~SbYM7M6n%4kjYtOu6fb~&aSCkG90!fi!1)XIeD6-_p(srmS92un4fCfDC>wwQ!IQO8jzd1;-QFuU{}1TjQz2x7xlx=Z>U=i&V4YFiz_N?U`(T`Sb|_z-ID#^Fylb65T$-bDTO- z6U$RRS)_ZNKcAI>k9sR})th56P*b2yX0UOhy7pyiQBpV54|{NYNYUh2QZMB$uFu0} zGd(!le2OUXiEl~-n}(Bm0w(N>(2iW!63SZWZ8>lNmc2b4o^ls(e|EoP^K0s(A-8`i zJ0xdXov#?mHn*k>w>@wn%~2hA z=lwLde{PVnmKI!tJ^9mLRfi2OGs-ee%GZQjo40pRq#D63&)qb)X8UTkO)o}PLqpMMm$0>?}4ly z+Fxs{6AFN;9e52uh{f)%j>fMWv4*QDO|UV;d!ECZjnp98AES-wBNX#N{KEdz+*sic zsIzZg2Uv9F7X0K%6pnaLA1_P^bu}~jaDl0;L}96Z9L3Uz#+rPFc9nlJ7{M3Xzzd=` zEpy|GlYVR>`p9aTNM=Tbi-v;05}xX#&=UPv!B(P|@#8ph)yYhly+`jG?{k2>O!0>o z+0vWBDfJ)Y2l*=^^sj@fBPT2)6q);Ts=-aRIn518Z>pqfE?c75*~cY47I$2^U`2f< z3K}yOMun42*W`@BAubIUDAKJ}TK@f13AE`SKya~sh6O{!`u97L(?1g7(EbEU?=*uo zgJt;;PRkX-uJ|MBv>%zjEtoPJ+!Jl&p|?;|RS{%|LD}%hg8vm$TB#?F?6GgD z2V*w2Ach6g;r!(1_bYFt`V}C{XtpDUxk`B3Q8gad*}zA3A+<+~m+R09td1~R5Nu=5 zr363loBNA3lcraV4L71j&JUk)i#n*UJU{yNjc9{z2wd{&>@V2qnY^%ZDurY`#xwCuhRVcD<0M?mfr+HFPI?-4wYVvke zwuurtD8ZB6Lwc!VER(=el338`ZUdeAfG~la0>Ge{=?5N!7=pV)BxM?QN6%p*t^3ha z2#-lDb+7Y>rQsA5^&%7>Fq3+4qRl-WsDq*A=1~VG;Fk*F$B`3zd2|8t1nxxJhOUho zG7{|(U+GrF0#U+Q`H-hoD|v;rRVB}VpcCRR@LCU)Y_6^lLNs)E(6ed9k<5WD6nI}N zWGC-j1%;Ze)mWSt)>+@l&2+espB37!6Phm_uj7RR< zQ|z24)|g97W&0!L$QDUcjco(3OJAguZ)~slCo_ z_W-^lsBbOuxBwV&j0eqMt_5O%Hpcx?E=oQ`*6)NRZ$bHD9n%QE74OtckbQLQYjxF| z<&W!M@zE4;yPCgsul|uIL3;dFOu2(su;r=#;UIn*S@EE#S~q`iUpa${P9@$ZnUKHt zv2tUb*Ym7fu>Vo^=J$_LcNY)cLmTB|Gik(EqJbMNbzO3Cu*j>9CTkDN!z#<>$J;aK zZ9E??aft73A_~kN`o=6f(BpnLcb-IF+_5?u>2_X$EN3#zemS=Wrq(e%>;4S#LY-=b zT$MV?ippKv#!J>bY4c-YQ_J;^G-}F)cGf4(twqPC%GJ0&d^N2kEF65$Nou{}L=SGG z!kTFzeF%#jBqBOlW0{l+u2|hdC4nl=-~kaCOLIzK(zVQGXY)#C&n{cI$|33iBB^PD zMZIlRm8p5?!wscL3_{L| zS{z4?Fx}mqgtG%r(pWD#6+4c8QC^Md;lz@5d_}ppmLor!HPE>M%xfUC?c<1A})jPc3L%nP)`^%gosn?PhRD6&RA(aLJC^E41km-vxS za8AKy4@a7c{4CB4QP1Ib($M?~9GCdQTZ_@X3_`OF4L(R|S(Fc|{12%2@YJbz}XpePDtu$Ttb63W$k+G5jGSYd^ zMMNjN0NM8MNd7z<9q@BNJ5JY91jo%Qfgaoat`M?4HRlbrd$I;I06NsJ7dC8ex=5Fd zI*Qxk)NpobH%|alstut62g#T02Pb4UggF3%wEKR5UN=~~o)Jh#00*nw!A}m_Cf~h% zO}=aY2-++W*yJaiL0q+_j}0}Kv$7Op(#xmfD7}5qZ({mn*%*+b?U1DcNH*Ni@FVPp zB+D^58*t#_ea5ZX!DD3FQJct;l{?+pO-yc3Fx8p%UOrq`O4N4;8~oY(Ja6&IdH;{C zSGS&gH2O>zO<|`QbgzdgS=Mm!YW1$yKBO2%tpp%FsmG@p&70zas;C(M9Db=(@>qyJ z2V1A?7^kN72vI%0g!q(fZ=Zi`z9?Pe68%xSD!>u#g*%2cIotMMp-0^e(us1dWR(cj&PwUCo@X}As|*3Z%P77^qY|Nz7j3JQ(KQm&lO_C2 zZRikF82>xYlWprXs#`w-_=JcN_s{(i?v_9^Ao_J4BP-~5V_bh*Sgq8GcsrO%?1}mc zIC3^&jqwq)U-B6V1A&~|nOHlZ{N2ut{vy85Tv`6%0ylk#l57AEv*tR>^`hxn8Y?dc znep1Xf13QwN->JO_-R)^@sC5%9|gJ7da#LgE7_UFb;_&mQ)F^}g4BJ+Lsz8>%vDul zDLvGNK*?K2g57tytFLR;?6uM?W5`MK2nefDpp~RWfy|U(wXi5_{njD$$iVKzrPqH? z-kgP6qX7(kb7E1Z?dSo(P&c{XDPe`}Qoo#a(-X3IXTR-9-V#^))z)dc2ZWe%8$5uY z_LACZwN4^mEvJ_;5UG??vcL6BCoMgONy(>S}8V`&xal(BAI8d z)TXyO@reTU6smj#r!&x@mcAmlSQP2CAq>*^A+s?TsRsoZTZ(`a#2<)whbLqPW?}rv zUIMdy@LL_5*#>K(3aQNNXMI6VuBiQ-qnA53tDZ~Xo+2!?*?^??Qk04-^^=ve<(eAI zZm2}>W4L?)MjitA;Ra#tJZ2izh*IEGesPV95*+oK#Ic#E|9(u*z5E(;%|YnyOERSP zsrWM^V*8c3mNk57f}b8r{$74X8wdIMQjkg$*r$+_o422s_v+O2Fe;`F6TUyxU+!bt za`>|;=u!5lyr6!TyDx?pO4yJfq6CX`+9#Zk9%Q!mql=G}j4l$=$_CQtDpq&>s`y_VaLn z7$@Qzq8L!`ff>U8D%VtGdZ`luv2Cvc$GIj$8oz)VcR+?WK2cRr11LBzBI% zTZVB00zJT9PJ_#K80od$g~D75^KZ;KO(o>`Roiuv)%{py^%dnB{#sAl`=+Ae^M7h$ z;sVGqumBkM{=Z6#f%A{my~lL_VWWG@_FoL2#g3;@J`ejaXa48*^TcX#)BVu*hk?Qrh@8~ulG{&ssW{{xAw B%rXD~ literal 0 HcmV?d00001 diff --git a/recognition/3D-UNT 48790835/picture/loss.jpg b/recognition/3D-UNT 48790835/picture/loss.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cb6dd49ee251f321e922b0967c75002798931fe6 GIT binary patch literal 17862 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 Date: Mon, 28 Oct 2024 00:06:27 +1000 Subject: [PATCH 15/26] Delete recognition/3D-UNT 48790835/picture/1 --- recognition/3D-UNT 48790835/picture/1 | 1 - 1 file changed, 1 deletion(-) delete mode 100644 recognition/3D-UNT 48790835/picture/1 diff --git a/recognition/3D-UNT 48790835/picture/1 b/recognition/3D-UNT 48790835/picture/1 deleted file mode 100644 index 8b1378917..000000000 --- a/recognition/3D-UNT 48790835/picture/1 +++ /dev/null @@ -1 +0,0 @@ - From c264f939c896f4011e155b21b5283137d0523139 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:14:59 +1000 Subject: [PATCH 16/26] Add files via upload --- recognition/3D-UNT 48790835/README.md | 80 +++++++++++++-------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/recognition/3D-UNT 48790835/README.md b/recognition/3D-UNT 48790835/README.md index 69b9cbe68..c79f4e84b 100644 --- a/recognition/3D-UNT 48790835/README.md +++ b/recognition/3D-UNT 48790835/README.md @@ -1,55 +1,53 @@ -# Medical Image Segmentation Project +# 3D UNet for Prostate Segmentation -## Project Overview -This project aims to utilize a 3D U-Net network for the segmentation of medical images. By loading MRI medical images and their corresponding labels, the model is trained and evaluated on a test set. +## Introduction -## File Structure -Below are the main files in the project and their functionalities: +This project utilizes the 3D UNet architecture to train on the Prostate 3D dataset, aiming to achieve precise medical volumetric image segmentation. We evaluate the performance of the segmentation using the Dice similarity coefficient, targeting a minimum score of 0.7 for all labels on the test set. Image segmentation transforms a volumetric image into segmented areas represented by masks, which facilitates medical condition analysis, symptom prediction, and treatment planning. -1. **train.py**: - - This script is used for training the model. It loads the training dataset and labels, trains the 3D U-Net model using an optimizer and loss function, and performs validation after each training epoch. - - It uses a custom dataset `MedicalDataset3D` and implements Dice loss as the training objective. +## Background -2. **dataset.py**: - - Contains the custom dataset class `MedicalDataset3D`, which is used to load 3D medical images and optional labels. - - Provides functionality for loading image paths and supports data augmentation. +### UNet-3D -3. **utils.py**: - - Contains the `DiceLoss` class for calculating the Dice loss and functions for computing the Dice coefficient. - - This module is primarily used for calculating the model's accuracy during the evaluation process. +The 3D UNet is an extension of the original UNet architecture, which is widely used for segmenting 2D medical images. While the standard UNet processes 2D images, UNet-3D extends this functionality to volumetric (3D) images, allowing for more accurate segmentation of complex medical structures found in modalities like MRI or CT scans. -4. **modules.py**: - - Defines the structure of the 3D U-Net model, including convolutional layers, activation layers, and upsampling layers. - - The model is designed to extract features from the input images to improve segmentation accuracy. +UNet architecture leverages a combination of convolutional neural networks (CNNs) and skip connections, improving performance by combining high-resolution features from the contracting path with low-resolution context from the expansive path. This design maintains spatial information throughout the segmentation process, which is critical in the medical imaging field. -5. **predict.py**: - - This script is used to load test data and apply the trained model for predictions. It calculates and prints the average Dice coefficient on the test data while visualizing the input images, target labels, and prediction results. -## Usage Instructions +![3D U-Net Architecture](https://raw.githubusercontent.com/Han1zen/PatternAnalysis-2024/refs/heads/topic-recognition/recognition/3D-UNT%2048790835/picture/3D%20U-Net.webp) -### Environment Requirements -- Python 3.x -- PyTorch -- nibabel -- numpy -- tqdm -- matplotlib +### Dataset -### Data Preparation -Before using this project, please ensure you have the following data prepared: -- 3D medical images (NIfTI format) -- Corresponding label data +For this project, we will segment the downsampled Prostate 3D dataset. A sample code for loading and processing Nifti file formats is provided in Appendix B. Furthermore, we encourage the use of data augmentation libraries for TensorFlow (TF) or the appropriate transformations in PyTorch to enhance the robustness of the model. -### Training the Model -1. Modify the data paths in `train.py` to point to your dataset. -2. Run `train.py` to start training. +### Evaluation Metric -### Prediction -1. Set the path for the test data in `predict.py`. -2. Run `predict.py` to obtain prediction results and Dice coefficient evaluation. +We will employ the Dice similarity coefficient as our primary evaluation metric. The Dice coefficient measures the overlap between the predicted segmentation and the ground truth, mathematically expressed as: -## Contribution -Contributions of any kind are welcome! If you have suggestions or issues, please open an issue or pull request. +\[ \text{Dice} = \frac{2 |A \cap B|}{|A| + |B|} \] + +where \( A \) and \( B \) are the sets of predicted and ground truth regions respectively. A Dice coefficient of 0.7 or greater indicates a significant degree of accuracy in segmentation. + +## Objectives + +- Implement the 3D Improved UNet architecture for the Prostate dataset. +- Achieve a minimum Dice similarity coefficient of 0.7 for all labels on the test set. +- Utilize data augmentation techniques to improve model generalization. +- Load and preprocess Nifti file formats for volumetric data analysis. + +## Results + +### Training and Validation Loss + +![Training and Validation Loss](recognition/3D-UNT 48790835/picture/loss.jpg) + +- The **training loss** curve demonstrates a rapid decline in the early stages of training, indicating that the model is effectively learning and adapting to the training data. +- As training progresses, the loss stabilizes, ultimately reaching around **0.6**. This suggests that the model performs well on the training set and is capable of effective feature learning. + +- The **validation loss** curve also exhibits a downward trend, remaining relatively close to the training loss in the later stages of training. +- This indicates that the model has good generalization capabilities on the validation set, with no significant signs of overfitting. The validation loss stabilizes at approximately **0.62**, further supporting the model's effectiveness. + +### Dice Similarity Coefficient + +- The model achieves a **Dice similarity coefficient** of over **0.7** for all labels, meeting our established target. +- This indicates that the model performs excellently in the segmentation task, accurately identifying and segmenting different regions of the prostate. -## License -This project is licensed under the MIT License. See the LICENSE file for details. \ No newline at end of file From 864c2f18c806c58420c3a7e1cad8a1152f32a0d0 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:18:44 +1000 Subject: [PATCH 17/26] Add files via upload --- recognition/3D-UNT 48790835/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recognition/3D-UNT 48790835/README.md b/recognition/3D-UNT 48790835/README.md index c79f4e84b..13db0f616 100644 --- a/recognition/3D-UNT 48790835/README.md +++ b/recognition/3D-UNT 48790835/README.md @@ -38,7 +38,7 @@ where \( A \) and \( B \) are the sets of predicted and ground truth regions res ### Training and Validation Loss -![Training and Validation Loss](recognition/3D-UNT 48790835/picture/loss.jpg) +![Training and Validation Loss](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/loss.jpg#:~:text=U%2DNet.webp-,loss,-.jpg) - The **training loss** curve demonstrates a rapid decline in the early stages of training, indicating that the model is effectively learning and adapting to the training data. - As training progresses, the loss stabilizes, ultimately reaching around **0.6**. This suggests that the model performs well on the training set and is capable of effective feature learning. From 66dbcd481ddeb0f6ac2db6d0209fc4438e46cafe Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:24:28 +1000 Subject: [PATCH 18/26] Add files via upload --- recognition/3D-UNT 48790835/picture/dice.png | Bin 0 -> 16105 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 recognition/3D-UNT 48790835/picture/dice.png diff --git a/recognition/3D-UNT 48790835/picture/dice.png b/recognition/3D-UNT 48790835/picture/dice.png new file mode 100644 index 0000000000000000000000000000000000000000..2de00cc45ca54e22f811166ecc2ee791474e4863 GIT binary patch literal 16105 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 Date: Mon, 28 Oct 2024 00:26:54 +1000 Subject: [PATCH 19/26] Add files via upload --- recognition/3D-UNT 48790835/picture/README.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 recognition/3D-UNT 48790835/picture/README.md diff --git a/recognition/3D-UNT 48790835/picture/README.md b/recognition/3D-UNT 48790835/picture/README.md new file mode 100644 index 000000000..59355a46e --- /dev/null +++ b/recognition/3D-UNT 48790835/picture/README.md @@ -0,0 +1,56 @@ +# 3D UNet for Prostate Segmentation + +## Introduction + +This project utilizes the 3D UNet architecture to train on the Prostate 3D dataset, aiming to achieve precise medical volumetric image segmentation. We evaluate the performance of the segmentation using the Dice similarity coefficient, targeting a minimum score of 0.7 for all labels on the test set. Image segmentation transforms a volumetric image into segmented areas represented by masks, which facilitates medical condition analysis, symptom prediction, and treatment planning. + +## Background + +### UNet-3D + +The 3D UNet is an extension of the original UNet architecture, which is widely used for segmenting 2D medical images. While the standard UNet processes 2D images, UNet-3D extends this functionality to volumetric (3D) images, allowing for more accurate segmentation of complex medical structures found in modalities like MRI or CT scans. + +UNet architecture leverages a combination of convolutional neural networks (CNNs) and skip connections, improving performance by combining high-resolution features from the contracting path with low-resolution context from the expansive path. This design maintains spatial information throughout the segmentation process, which is critical in the medical imaging field. + + +![3D U-Net Architecture](https://raw.githubusercontent.com/Han1zen/PatternAnalysis-2024/refs/heads/topic-recognition/recognition/3D-UNT%2048790835/picture/3D%20U-Net.webp) + +### Dataset + +For this project, we will segment the downsampled Prostate 3D dataset. A sample code for loading and processing Nifti file formats is provided in Appendix B. Furthermore, we encourage the use of data augmentation libraries for TensorFlow (TF) or the appropriate transformations in PyTorch to enhance the robustness of the model. + +### Evaluation Metric + +We will employ the Dice similarity coefficient as our primary evaluation metric. The Dice coefficient measures the overlap between the predicted segmentation and the ground truth, mathematically expressed as: + +\[ \text{Dice} = \frac{2 |A \cap B|}{|A| + |B|} \] + +where \( A \) and \( B \) are the sets of predicted and ground truth regions respectively. A Dice coefficient of 0.7 or greater indicates a significant degree of accuracy in segmentation. + +## Objectives + +- Implement the 3D Improved UNet architecture for the Prostate dataset. +- Achieve a minimum Dice similarity coefficient of 0.7 for all labels on the test set. +- Utilize data augmentation techniques to improve model generalization. +- Load and preprocess Nifti file formats for volumetric data analysis. + +## Results + +### Training and Validation Loss + +![Training and Validation Loss](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/loss.jpg#:~:text=U%2DNet.webp-,loss,-.jpg) + +- The **training loss** curve demonstrates a rapid decline in the early stages of training, indicating that the model is effectively learning and adapting to the training data. +- As training progresses, the loss stabilizes, ultimately reaching around **0.6**. This suggests that the model performs well on the training set and is capable of effective feature learning. + +- The **validation loss** curve also exhibits a downward trend, remaining relatively close to the training loss in the later stages of training. +- This indicates that the model has good generalization capabilities on the validation set, with no significant signs of overfitting. The validation loss stabilizes at approximately **0.62**, further supporting the model's effectiveness. + +### Dice Similarity Coefficient + +![](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/dice.png#:~:text=dice.-,png,-loss.jpg) +- The model achieves a **Dice similarity coefficient** of over **0.7** for all labels, meeting our established target. +- This indicates that the model performs excellently in the segmentation task, accurately identifying and segmenting different regions of the prostate. + + + From 5689ddc7a6a9996e660d7ff786c4f8e250d484dc Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:34:31 +1000 Subject: [PATCH 20/26] Add files via upload --- recognition/3D-UNT 48790835/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/recognition/3D-UNT 48790835/README.md b/recognition/3D-UNT 48790835/README.md index 13db0f616..daacfcf0c 100644 --- a/recognition/3D-UNT 48790835/README.md +++ b/recognition/3D-UNT 48790835/README.md @@ -34,6 +34,15 @@ where \( A \) and \( B \) are the sets of predicted and ground truth regions res - Utilize data augmentation techniques to improve model generalization. - Load and preprocess Nifti file formats for volumetric data analysis. +## Quick Start + +To get started with the 3D UNet model for prostate segmentation, follow these steps: + +1. **Clone the Repository**: Clone the repository to your local machine. +2. **Install Dependencies**: Ensure you have the required libraries installed. +3. **Prepare the Dataset**: Download the Prostate 3D dataset and place it in the `data/` directory. +4. **Run Training**: Execute the training script to begin training the model on the Prostate 3D dataset. + ## Results ### Training and Validation Loss @@ -48,6 +57,12 @@ where \( A \) and \( B \) are the sets of predicted and ground truth regions res ### Dice Similarity Coefficient +![Dice](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/dice.png#:~:text=dice.-,png,-loss.jpg) - The model achieves a **Dice similarity coefficient** of over **0.7** for all labels, meeting our established target. - This indicates that the model performs excellently in the segmentation task, accurately identifying and segmenting different regions of the prostate. + +## References + +1. Sik-Ho Tsang. "Review: 3D U-Net — Volumetric Segmentation (Medical Image Segmentation)." [Towards Data Science](https://towardsdatascience.com/review-3d-u-net-volumetric-segmentation-medical-image-segmentation-8b592560fac1). + From cc645c95ed41d76d8fb10dd3be7e79dd44d57e7a Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:34:55 +1000 Subject: [PATCH 21/26] Delete recognition/3D-UNT 48790835/train_loss_dice1.png --- .../3D-UNT 48790835/train_loss_dice1.png | Bin 49211 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 recognition/3D-UNT 48790835/train_loss_dice1.png diff --git a/recognition/3D-UNT 48790835/train_loss_dice1.png b/recognition/3D-UNT 48790835/train_loss_dice1.png deleted file mode 100644 index 2f2a6242c6ebc9e44d5a003450b3d15c9b3e8940..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49211 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 From 906d644d0fd6f43ff842d038bba07078468f32ff Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 00:42:44 +1000 Subject: [PATCH 22/26] Add files via upload From 922b7dc70b9ae650bb3fb74eef410fbd741fe05e Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 13:24:04 +1000 Subject: [PATCH 23/26] Delete recognition/3D-UNT 48790835/picture/README.md --- recognition/3D-UNT 48790835/picture/README.md | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 recognition/3D-UNT 48790835/picture/README.md diff --git a/recognition/3D-UNT 48790835/picture/README.md b/recognition/3D-UNT 48790835/picture/README.md deleted file mode 100644 index 59355a46e..000000000 --- a/recognition/3D-UNT 48790835/picture/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# 3D UNet for Prostate Segmentation - -## Introduction - -This project utilizes the 3D UNet architecture to train on the Prostate 3D dataset, aiming to achieve precise medical volumetric image segmentation. We evaluate the performance of the segmentation using the Dice similarity coefficient, targeting a minimum score of 0.7 for all labels on the test set. Image segmentation transforms a volumetric image into segmented areas represented by masks, which facilitates medical condition analysis, symptom prediction, and treatment planning. - -## Background - -### UNet-3D - -The 3D UNet is an extension of the original UNet architecture, which is widely used for segmenting 2D medical images. While the standard UNet processes 2D images, UNet-3D extends this functionality to volumetric (3D) images, allowing for more accurate segmentation of complex medical structures found in modalities like MRI or CT scans. - -UNet architecture leverages a combination of convolutional neural networks (CNNs) and skip connections, improving performance by combining high-resolution features from the contracting path with low-resolution context from the expansive path. This design maintains spatial information throughout the segmentation process, which is critical in the medical imaging field. - - -![3D U-Net Architecture](https://raw.githubusercontent.com/Han1zen/PatternAnalysis-2024/refs/heads/topic-recognition/recognition/3D-UNT%2048790835/picture/3D%20U-Net.webp) - -### Dataset - -For this project, we will segment the downsampled Prostate 3D dataset. A sample code for loading and processing Nifti file formats is provided in Appendix B. Furthermore, we encourage the use of data augmentation libraries for TensorFlow (TF) or the appropriate transformations in PyTorch to enhance the robustness of the model. - -### Evaluation Metric - -We will employ the Dice similarity coefficient as our primary evaluation metric. The Dice coefficient measures the overlap between the predicted segmentation and the ground truth, mathematically expressed as: - -\[ \text{Dice} = \frac{2 |A \cap B|}{|A| + |B|} \] - -where \( A \) and \( B \) are the sets of predicted and ground truth regions respectively. A Dice coefficient of 0.7 or greater indicates a significant degree of accuracy in segmentation. - -## Objectives - -- Implement the 3D Improved UNet architecture for the Prostate dataset. -- Achieve a minimum Dice similarity coefficient of 0.7 for all labels on the test set. -- Utilize data augmentation techniques to improve model generalization. -- Load and preprocess Nifti file formats for volumetric data analysis. - -## Results - -### Training and Validation Loss - -![Training and Validation Loss](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/loss.jpg#:~:text=U%2DNet.webp-,loss,-.jpg) - -- The **training loss** curve demonstrates a rapid decline in the early stages of training, indicating that the model is effectively learning and adapting to the training data. -- As training progresses, the loss stabilizes, ultimately reaching around **0.6**. This suggests that the model performs well on the training set and is capable of effective feature learning. - -- The **validation loss** curve also exhibits a downward trend, remaining relatively close to the training loss in the later stages of training. -- This indicates that the model has good generalization capabilities on the validation set, with no significant signs of overfitting. The validation loss stabilizes at approximately **0.62**, further supporting the model's effectiveness. - -### Dice Similarity Coefficient - -![](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/dice.png#:~:text=dice.-,png,-loss.jpg) -- The model achieves a **Dice similarity coefficient** of over **0.7** for all labels, meeting our established target. -- This indicates that the model performs excellently in the segmentation task, accurately identifying and segmenting different regions of the prostate. - - - From bfaf36dfc34f21820ad21903d540bf604341293f Mon Sep 17 00:00:00 2001 From: Han1zen Date: Mon, 28 Oct 2024 13:24:18 +1000 Subject: [PATCH 24/26] Add files via upload --- .../picture/train_loss_and_valid_loss.png | Bin 0 -> 85800 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 recognition/3D-UNT 48790835/picture/train_loss_and_valid_loss.png diff --git a/recognition/3D-UNT 48790835/picture/train_loss_and_valid_loss.png b/recognition/3D-UNT 48790835/picture/train_loss_and_valid_loss.png new file mode 100644 index 0000000000000000000000000000000000000000..3411ac1b1a35d3c28c512be3437cdeae9c0d44a3 GIT binary patch literal 85800 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 Date: Mon, 28 Oct 2024 13:26:22 +1000 Subject: [PATCH 25/26] Add files via upload --- recognition/3D-UNT 48790835/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recognition/3D-UNT 48790835/README.md b/recognition/3D-UNT 48790835/README.md index daacfcf0c..4b3bd372a 100644 --- a/recognition/3D-UNT 48790835/README.md +++ b/recognition/3D-UNT 48790835/README.md @@ -47,7 +47,7 @@ To get started with the 3D UNet model for prostate segmentation, follow these st ### Training and Validation Loss -![Training and Validation Loss](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/loss.jpg#:~:text=U%2DNet.webp-,loss,-.jpg) +![Training and Validation Loss](https://github.com/Han1zen/PatternAnalysis-2024/blob/topic-recognition/recognition/3D-UNT%2048790835/picture/train_loss_and_valid_loss.png#:~:text=loss.jpg-,train_loss_and_valid_loss,-.png) - The **training loss** curve demonstrates a rapid decline in the early stages of training, indicating that the model is effectively learning and adapting to the training data. - As training progresses, the loss stabilizes, ultimately reaching around **0.6**. This suggests that the model performs well on the training set and is capable of effective feature learning. From 80795ca9aa6234e51f5b8d247258e6237ff84bf7 Mon Sep 17 00:00:00 2001 From: Han1zen Date: Tue, 12 Nov 2024 18:56:49 +1000 Subject: [PATCH 26/26] Add files via upload --- recognition/3D-UNT 48790835/dataset.py | 41 ++++-- recognition/3D-UNT 48790835/modules.py | 161 +++++++++++----------- recognition/3D-UNT 48790835/predict.py | 6 +- recognition/3D-UNT 48790835/train.py | 179 ++++++++++++++++--------- 4 files changed, 229 insertions(+), 158 deletions(-) diff --git a/recognition/3D-UNT 48790835/dataset.py b/recognition/3D-UNT 48790835/dataset.py index 076cea95d..2af554eb8 100644 --- a/recognition/3D-UNT 48790835/dataset.py +++ b/recognition/3D-UNT 48790835/dataset.py @@ -11,7 +11,6 @@ EnsureChannelFirstd, ScaleIntensityd, RandRotate90d, - RandShiftIntensityd, ) # Transforms for training data: load, resize, and apply random flips and rotations @@ -37,7 +36,7 @@ EnsureTyped(keys=("image", "label"), dtype=torch.float32), ]) -class MRIDataset_pelvis(Dataset): +class CustomDataset(Dataset): """ Dataset class for reading pelvic MRI data. """ @@ -76,28 +75,48 @@ def __getitem__(self, index): if self.mode == 'train': augmented = self.train_transform({'image': img_path, 'label': label_path}) - return augmented['image'], augmented['label'] + image = augmented['image'] + label = augmented['label'] + + # 确保图像和标签是4D张量 + if image.dim() == 5: # 如果是5D张量 + image = image.squeeze(1) # 去掉通道维度,变为4D张量 (x, y, z) + + if label.dim() == 5: # 如果是5D张量 + label = label.squeeze(1) # 去掉通道维度,变为4D张量 (x, y, z) + + return image, label if self.mode == 'test': augmented = self.test_transform({'image': img_path, 'label': label_path}) - return augmented['image'], augmented['label'] + image = augmented['image'] + label = augmented['label'] + + # 确保图像和标签是4D张量 + if image.dim() == 5: # 如果是5D张量 + image = image.squeeze(1) # 去掉通道维度,变为4D张量 (x, y, z) + + if label.dim() == 5: # 如果是5D张量 + label = label.squeeze(1) # 去掉通道维度,变为4D张量 (x, y, z) + + return image, label if __name__ == '__main__': # Test the dataset - test_dataset = MRIDataset_pelvis(mode='test', dataset_path=r"path_to_your_dataset") + test_dataset = CustomDataset(mode='test', dataset_path=r"path_to_your_dataset") test_dataloader = DataLoader(dataset=test_dataset, batch_size=2, shuffle=False) print(len(test_dataset)) for batch_ndx, sample in enumerate(test_dataloader): print('test') - print(sample[0].shape) - print(sample[1].shape) + print(sample[0].shape) # 应该打印 (batch_size, channels, x, y, z) + print(sample[1].shape) # 应该打印 (batch_size, channels, x, y, z) break - train_dataset = MRIDataset_pelvis(mode='train', dataset_path=r"path_to_your_dataset") + train_dataset = CustomDataset(mode='train', dataset_path=r"path_to_your_dataset") train_dataloader = DataLoader(dataset=train_dataset, batch_size=2, shuffle=False) for batch_ndx, sample in enumerate(train_dataloader): print('train') - print(sample[0].shape) - print(sample[1].shape) - break + print(sample[0].shape) # 应该打印 (batch_size, channels, x, y, z) + print(sample[1].shape) # 应该打印 (batch_size, channels, x, y, z) + break \ No newline at end of file diff --git a/recognition/3D-UNT 48790835/modules.py b/recognition/3D-UNT 48790835/modules.py index 23c731959..9e2d0b3f9 100644 --- a/recognition/3D-UNT 48790835/modules.py +++ b/recognition/3D-UNT 48790835/modules.py @@ -1,93 +1,92 @@ import torch import torch.nn as nn import torch.nn.functional as F -from dataset import MRIDataset_pelvis -from torch.utils.data import DataLoader +class DoubleConv(nn.Module): + """(convolution => [BN] => ReLU) * 2""" -class UNet3D(nn.Module): - """3D U-Net model for segmentation tasks.""" - - def __init__(self, in_channel=1, out_channel=6): - super(UNet3D, self).__init__() - - # Encoder layers with Batch Normalization - self.encoder1 = self.conv_block(in_channel, 32) - self.encoder2 = self.conv_block(32, 64) - self.encoder3 = self.conv_block(64, 128) - self.encoder4 = self.conv_block(128, 256) - - # Decoder layers with Dropout - self.decoder2 = self.deconv_block(256, 128) - self.decoder3 = self.deconv_block(128, 64) - self.decoder4 = self.deconv_block(64, 32) - self.decoder5 = nn.Conv3d(32, out_channel, kernel_size=1) # Output layer - - def conv_block(self, in_channels, out_channels): - return nn.Sequential( + def __init__(self, in_channels, out_channels): + super().__init__() + self.double_conv = nn.Sequential( nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1), nn.BatchNorm3d(out_channels), - nn.LeakyReLU(inplace=True) + nn.ReLU(inplace=True), + nn.Conv3d(out_channels, out_channels, kernel_size=3, padding=1), + nn.BatchNorm3d(out_channels), + nn.ReLU(inplace=True) ) - def deconv_block(self, in_channels, out_channels): - return nn.Sequential( - nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1), - nn.BatchNorm3d(out_channels), - nn.LeakyReLU(inplace=True), - nn.Dropout(0.5) + def forward(self, x): + return self.double_conv(x) + +class Down(nn.Module): + """Downscaling with maxpool then double conv""" + + def __init__(self, in_channels, out_channels): + super().__init__() + self.maxpool_conv = nn.Sequential( + nn.MaxPool3d(2), + DoubleConv(in_channels, out_channels) ) def forward(self, x): - # Encoder part - out1 = self.encoder1(x) - out2 = self.encoder2(F.max_pool3d(out1, kernel_size=2, stride=2)) - out3 = self.encoder3(F.max_pool3d(out2, kernel_size=2, stride=2)) - out4 = self.encoder4(F.max_pool3d(out3, kernel_size=2, stride=2)) - - # Decoder part - out = F.interpolate(self.decoder2(out4), scale_factor=(2, 2, 2), mode='trilinear') - out = out + out3 - - out = F.interpolate(self.decoder3(out), scale_factor=(2, 2, 2), mode='trilinear') - out = out + out2 - - out = F.interpolate(self.decoder4(out), scale_factor=(2, 2, 2), mode='trilinear') - out = out + out1 - - out = self.decoder5(out) # No activation here, will apply softmax during evaluation - return out - - -class DiceLoss(nn.Module): - """Dice loss for evaluating segmentation accuracy.""" - - def __init__(self, smooth=1): - super(DiceLoss, self).__init__() - self.smooth = smooth - - def forward(self, inputs, targets): - assert inputs.shape == targets.shape, f"Shapes don't match: {inputs.shape} != {targets.shape}" - inputs = inputs[:, 1:] # Skip background class - targets = targets[:, 1:] # Skip background class - axes = tuple(range(2, len(inputs.shape))) # Sum over elements per sample and per class - intersection = torch.sum(inputs * targets, axes) - addition = torch.sum(inputs ** 2 + targets ** 2, axes) - return 1 - torch.mean((2 * intersection + self.smooth) / (addition + self.smooth)) - - -if __name__ == '__main__': - # Testing the dataset and model - test_dataset = MRIDataset_pelvis(mode='test', dataset_path=r"path_to_your_dataset") - test_dataloader = DataLoader(dataset=test_dataset, batch_size=2, shuffle=False) - model = UNet3D(in_channel=1, out_channel=6) - - for batch_ndx, sample in enumerate(test_dataloader): - print('Test batch:') - print('Image shape:', sample[0].shape) - print('Label shape:', sample[1].shape) - output = model(sample[0]) - print('Output shape:', output.shape) - - labely = torch.nn.functional.one_hot(sample[1].squeeze(1).long(), num_classes=6).permute(0, 4, 1, 2, 3).float() - break + return self.maxpool_conv(x) + +class Up(nn.Module): + """Upscaling then double conv""" + + def __init__(self, in_channels, out_channels, bilinear=True): + super().__init__() + + # if bilinear, use the normal convolutions to reduce the number of channels + if bilinear: + self.up = nn.Upsample(scale_factor=2, mode='trilinear', align_corners=True) + self.conv = DoubleConv(in_channels, out_channels) + else: + self.up = nn.ConvTranspose3d(in_channels, in_channels // 2, kernel_size=2, stride=2) + self.conv = DoubleConv(in_channels, out_channels) + + def forward(self, x1, x2): + x1 = self.up(x1) + # input is CHW + diffZ = x2.size()[2] - x1.size()[2] + diffY = x2.size()[3] - x1.size()[3] + diffX = x2.size()[4] - x1.size()[4] + + x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2, + diffY // 2, diffY - diffY // 2, + diffZ // 2, diffZ - diffZ // 2]) + + x = torch.cat([x2, x1], dim=1) + return self.conv(x) + +class UNet3D(nn.Module): + def __init__(self, in_channels=1, out_channels=6): + super(UNet3D, self).__init__() + self.in_channels = in_channels + self.out_channels = out_channels + + self.inc = DoubleConv(in_channels, 64) + self.down1 = Down(64, 128) + self.down2 = Down(128, 256) + self.down3 = Down(256, 512) + self.down4 = Down(512, 512) + self.up1 = Up(512, 256) + self.up2 = Up(256, 128) + self.up3 = Up(128, 64) + self.up4 = Up(64, 64) + self.outc = nn.Conv3d(64, out_channels, kernel_size=1) + + def forward(self, x): + + x1 = self.inc(x) + x2 = self.down1(x1) + x3 = self.down2(x2) + x4 = self.down3(x3) + x5 = self.down4(x4) + x = self.up1(x5, x4) + x = self.up2(x, x3) + x = self.up3(x, x2) + x = self.up4(x, x1) + logits = self.outc(x) + return logits \ No newline at end of file diff --git a/recognition/3D-UNT 48790835/predict.py b/recognition/3D-UNT 48790835/predict.py index ce10f53ba..ac204e03a 100644 --- a/recognition/3D-UNT 48790835/predict.py +++ b/recognition/3D-UNT 48790835/predict.py @@ -3,7 +3,7 @@ import random import argparse from modules import UNet3D -from dataset import MRIDataset_pelvis +from dataset import Dataset from torch.utils.data import DataLoader import torch.nn as nn @@ -15,11 +15,11 @@ # Load the model model = UNet3D(in_channel=1, out_channel=6).cuda() -model.load_state_dict(torch.load(r'epoch_19_lossdice1.pth')) +model.load_state_dict(torch.load(r'epoch_2_lossdice1.pth')) model.eval() # Define the test dataloader -test_dataset = MRIDataset_pelvis(mode='test', dataset_path=r'C:\Users\111\Desktop\3710\新建文件夹\数据集\Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-\data\HipMRI_study_complete_release_v1') +test_dataset = Dataset(mode='test', dataset_path=r'C:\Users\111\Desktop\3710\新建文件夹\数据集\Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-\data\HipMRI_study_complete_release_v1') test_dataloader = DataLoader(dataset=test_dataset, batch_size=1, shuffle=False) # Define weighted Dice loss function diff --git a/recognition/3D-UNT 48790835/train.py b/recognition/3D-UNT 48790835/train.py index e0649b6f7..a0d9e9a09 100644 --- a/recognition/3D-UNT 48790835/train.py +++ b/recognition/3D-UNT 48790835/train.py @@ -1,30 +1,33 @@ +import os import torch import numpy as np import random import argparse from modules import UNet3D -from dataset import MRIDataset_pelvis +from dataset import CustomDataset # 确保这里是正确的类名 from torch.utils.data import DataLoader import time import matplotlib.pyplot as plt +import torchio as tio - -## set random seed +# Set random seed seed = 42 torch.manual_seed(seed) np.random.seed(seed) random.seed(seed) parser = argparse.ArgumentParser() -parser.add_argument('--lr',default=0.001) -parser.add_argument('--epoch',default=20) -parser.add_argument('--device',default='cuda') -parser.add_argument('--loss',default='dice') -parser.add_argument('--dataset_root', type=str, default=r'C:\Users\111\Desktop\3710\新建文件夹\数据集\Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-\data\HipMRI_study_complete_release_v1', help='Root directory of the dataset') +parser.add_argument('--lr', default=0.001) +parser.add_argument('--epoch', default=20) +parser.add_argument('--device', default='cuda') +parser.add_argument('--loss', default='dice') +parser.add_argument('--dataset_root', type=str, + default=r'C:\Users\111\Desktop\3710\新建文件夹\数据集\Labelled_weekly_MR_images_of_the_male_pelvis-Xken7gkM-\data\HipMRI_study_complete_release_v1', + help='Root directory of the dataset') args = parser.parse_args() -##define the model -model=UNet3D(in_channel=1, out_channel=6).cuda() +# Define the model +model = UNet3D(in_channels=1, out_channels=6).to(args.device) class DiceLoss(torch.nn.Module): def __init__(self, smooth=1): @@ -33,80 +36,130 @@ def __init__(self, smooth=1): def forward(self, inputs, targets): assert inputs.shape == targets.shape, f"Shapes don't match {inputs.shape} != {targets.shape}" - inputs = inputs[:,1:] # skip background class - targets = targets[:,1:] # skip background class - axes = tuple(range(2, len(inputs.shape))) # sum over elements per sample and per class + + # Skip background class + inputs = inputs[:, 1:] + targets = targets[:, 1:] + + # Sum over elements per sample and per class + axes = tuple(range(2, len(inputs.shape))) # 这里的范围从2开始,适应5D张量 intersection = torch.sum(inputs * targets, axes) addition = torch.sum(torch.square(inputs) + torch.square(targets), axes) - return 1 - torch.mean((2 * intersection + self.smooth) / (addition + self.smooth)) -##define the loss -if args.loss =='mse': - criterion = torch.nn.MSELoss().cuda() -elif args.loss =='dice': - criterion = DiceLoss().cuda() -elif args.loss =='ce': - criterion = torch.nn.CrossEntropyLoss().cuda() -optimizer = torch.optim.Adam(model.parameters(),lr=args.lr) + # 计算Dice损失 + dice_score = (2 * intersection + self.smooth) / (addition + self.smooth) + return 1 - torch.mean(dice_score) + +criterion = DiceLoss().to(args.device) + +# Define the data augmentation class +class Augment: + def __init__(self): + self.shrink = tio.CropOrPad((16, 32, 32)) + self.flip = tio.transforms.RandomFlip(0, flip_probability=0.5) + + def __call__(self, image, mask): + image = self.shrink(image) + mask = self.shrink(mask) + image = self.flip(image) + mask = self.flip(mask) + return image, mask + +# Define the train and test dataloaders +train_dataset = CustomDataset(mode='train', dataset_path=args.dataset_root) +train_dataloader = DataLoader(dataset=train_dataset, batch_size=1, shuffle=True) +test_dataset = CustomDataset(mode='test', dataset_path=args.dataset_root) +test_dataloader = DataLoader(dataset=test_dataset, batch_size=1, shuffle=False) + +optimizer = torch.optim.Adam(model.parameters(), lr=args.lr) train_loss = [] valid_loss = [] train_epochs_loss = [] valid_epochs_loss = [] -##define the train-dataloader and test_dataloader -train_dataset = MRIDataset_pelvis(mode='train',dataset_path=args.dataset_root) -train_dataloader = DataLoader(dataset=train_dataset, batch_size=1, shuffle=True) -test_dataset = MRIDataset_pelvis(mode='test',dataset_path=args.dataset_root) -test_dataloader = DataLoader(dataset=test_dataset, batch_size=1, shuffle=False) +# Initialize data augmentation +augment = Augment() -##the training and valid process -start_time =time.time() +# The training and validation process +start_time = time.time() for epoch in range(args.epoch): model.train() train_epoch_loss = [] - for idx,(data_x,data_y) in enumerate(train_dataloader): - data_x = data_x.to(torch.float32).cuda() - data_y = data_y.to(torch.float32).cuda() - labely=torch.nn.functional.one_hot(data_y.squeeze(1).long(),num_classes=6).permute(0,4,1,2,3).float() + for idx, (data_x, data_y) in enumerate(train_dataloader): + data_x = data_x.to(torch.float32).to(args.device) + data_y = data_y.to(torch.float32).to(args.device) + + # Ensure data_x is 5D + if data_x.dim() == 4: # If it's a 4D tensor + data_x = data_x.unsqueeze(1) # Add a channel dimension + + data_x, data_y = augment(data_x, data_y) # Apply augmentation + + # Ensure data_y is 5D + if data_y.dim() == 4: # If it's a 4D tensor + data_y = data_y.unsqueeze(1) # Add a channel dimension + + labely = torch.nn.functional.one_hot(data_y.squeeze(1).long(), num_classes=6).permute(0, 4, 1, 2, 3).float().to(args.device) outputs = model(data_x) optimizer.zero_grad() - loss = criterion(labely,outputs) + loss = criterion(outputs, labely) loss.backward() optimizer.step() train_epoch_loss.append(loss.item()) train_loss.append(loss.item()) - if idx%(len(train_dataloader)//2)==0: - epoch_time=time.time()-start_time - print("epoch={}/{},{}/{}of train, loss={} epoch time{}".format( - epoch, args.epoch, idx, len(train_dataloader),loss.item(),epoch_time)) + train_epochs_loss.append(np.average(train_epoch_loss)) - epoch_time=time.time()-start_time - print(f'epoch{epoch}:',train_epochs_loss) - if epoch%1==0: + epoch_time = time.time() - start_time + print(f'Epoch {epoch}: Train Loss: {train_epochs_loss[-1]:.4f}') + + if epoch % 1 == 0: model.eval() valid_epoch_loss = [] - for idx,(data_x,data_y) in enumerate(test_dataloader): - data_x = data_x.to(torch.float32).to(args.device) - data_y = data_y.to(torch.float32).to(args.device) - labely=torch.nn.functional.one_hot(data_y.squeeze(1).long(),num_classes=6).permute(0,4,1,2,3).float() - outputs = model(data_x) - loss = criterion(outputs,labely) - valid_epoch_loss.append(loss.item()) - valid_loss.append(loss.item()) + with torch.no_grad(): + for idx, (data_x, data_y) in enumerate(test_dataloader): + data_x = data_x.to(torch.float32).to(args.device) + data_y = data_y.to(torch.float32).to(args.device) + + # Ensure data_x is 5D + if data_x.dim() == 4: # If it's a 4D tensor + data_x = data_x.unsqueeze(1) # Add a channel dimension + + # Ensure data_y is 5D + if data_y.dim() == 4: # If it's a 4D tensor + data_y = data_y.unsqueeze(1) # Add a channel dimension + + labely = torch.nn.functional.one_hot(data_y.squeeze(1).long(), num_classes=6).permute(0, 4, 1, 2, 3).float().to(args.device) + outputs = model(data_x) + loss = criterion(outputs, labely) + valid_epoch_loss.append(loss.item()) + valid_loss.append(loss.item()) + valid_epochs_loss.append(np.average(valid_epoch_loss)) - ##save the trained model - torch.save(model.state_dict(),f'epoch_{epoch}_loss{args.loss}1.pth') - -#plot the loss graph -plt.figure(figsize=(12,4)) -plt.subplot(121) -plt.plot(train_loss[:]) -plt.title(f"train_loss_{args.loss}") -plt.subplot(122) -plt.plot(train_epochs_loss[1:],'-o',label="train_loss") -plt.plot(valid_epochs_loss[1:],'-o',label="valid_loss") -plt.title("epochs_loss") -plt.legend() -plt.savefig(f"train_loss_{args.loss}1.png") \ No newline at end of file + # Save the trained model + torch.save(model.state_dict(), f'epoch_{epoch}_loss{args.loss}.pth') + +# Plotting the training and validation loss +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6)) + +# Left plot: Training loss +ax1.plot(train_loss, label='Train Loss (Dice)', color='blue', linewidth=2) +ax1.set_title("Train Loss (Dice)", fontsize=16) +ax1.set_xlabel("Iterations", fontsize=14) +ax1.set_ylabel("Loss", fontsize=14) +ax1.legend() +ax1.grid(True) + +# Right plot: Training and validation loss comparison +ax2.plot(np.arange(0, len(train_epochs_loss)), train_epochs_loss, '-o', label='Epoch Train Loss', color='orange', markersize=4) +ax2.plot(np.arange(0, len(valid_epochs_loss)), valid_epochs_loss, '-o', label='Epoch Valid Loss', color='green', markersize=4) +ax2.set_title("Train and Validation Loss", fontsize=16) +ax2.set_xlabel("Epochs", fontsize=14) +ax2.set_ylabel("Loss", fontsize=14) +ax2.legend() +ax2.grid(True) + +plt.tight_layout() +plt.savefig(f"train_loss_and_valid_loss.png") +plt.show() \ No newline at end of file