From 0faa320a05b827259fe1ad44d6091ef6c881fa2d Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Tue, 22 Oct 2024 15:25:24 +1000 Subject: [PATCH 01/28] topic-recognition - initial commit including project report readme file and folder creation --- recognition/2d_unet_s46974426/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 recognition/2d_unet_s46974426/README.md diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md new file mode 100644 index 000000000..95ea2c6a0 --- /dev/null +++ b/recognition/2d_unet_s46974426/README.md @@ -0,0 +1 @@ +README file for Report 2D UNet report \ No newline at end of file From 1da32d86003f123e6f67eabccd931a76b2eeaf7d Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Tue, 22 Oct 2024 15:28:08 +1000 Subject: [PATCH 02/28] topic-recognition - created blank python files for modules, dataset, train and predict --- recognition/2d_unet_s46974426/dataset.py | 0 recognition/2d_unet_s46974426/modules.py | 0 recognition/2d_unet_s46974426/predict.py | 0 recognition/2d_unet_s46974426/train.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 recognition/2d_unet_s46974426/dataset.py create mode 100644 recognition/2d_unet_s46974426/modules.py create mode 100644 recognition/2d_unet_s46974426/predict.py create mode 100644 recognition/2d_unet_s46974426/train.py diff --git a/recognition/2d_unet_s46974426/dataset.py b/recognition/2d_unet_s46974426/dataset.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/2d_unet_s46974426/predict.py b/recognition/2d_unet_s46974426/predict.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py new file mode 100644 index 000000000..e69de29bb From 443b340b2079923e1dde17f6db0a4a1efad9964b Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Tue, 22 Oct 2024 15:55:41 +1000 Subject: [PATCH 03/28] topic-recognition - dataset python populated with 2D data loading functionality from Appendix 1 --- recognition/2d_unet_s46974426/dataset.py | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/recognition/2d_unet_s46974426/dataset.py b/recognition/2d_unet_s46974426/dataset.py index e69de29bb..10e59ffd9 100644 --- a/recognition/2d_unet_s46974426/dataset.py +++ b/recognition/2d_unet_s46974426/dataset.py @@ -0,0 +1,67 @@ +import numpy as np +import nibabel as nib +from tqdm import tqdm +import utils as utils + + +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 c in channels : + c = int(c) + res [... , c : c + 1][arr == c] = 1 + + return res + +# load medical image functions +def load_data_2D(imageNames, normImage=False, categorical=False, dtype = np.float32, getAffines = False , early_stop = False) : + ''' + Load medical image data from names , cases list provided into a list for each . + + This function pre - allocates 4D arrays for conv2d to avoid excessive memory & + usage . + + normImage : bool ( normalise the image 0.0 -1.0) + early_stop : Stop loading pre - maturely , leaves arrays mostly empty , for quick & + loading and testing scripts . + ''' + affines = [] + + # get fixed size + num = len(imageNames) + first_case = nib.load(imageNames[0]).get_fdata(caching = 'unchanged') + if len(first_case.shape) == 3: + first_case = first_case [: ,: ,0] # sometimes extra dims , remove + if categorical: + first_case = to_channels (first_case, dtype = dtype) + rows, cols, channels = first_case.shape + images = np.zeros((num, rows, cols, channels), dtype = dtype) + else : + rows, cols = first_case.shape + images = np.zeros((num , rows , cols), dtype = dtype) + + for i, inName in enumerate(tqdm(imageNames)): + niftiImage = nib.load(inName) + inImage = niftiImage.get_fdata(caching = 'unchanged') # read disk only + affine = niftiImage.affine + if len(inImage.shape) == 3: + inImage = inImage [: ,: ,0] # sometimes extra dims in HipMRI_study data + inImage = inImage.astype(dtype) + if normImage: + #~ inImage = inImage / np. linalg . norm ( inImage ) + #~ inImage = 255. * inImage / inImage . max () + inImage = (inImage - inImage.mean()) / inImage.std() + if categorical : + inImage = utils.to_channels(inImage, dtype = dtype) + images [i ,: ,: ,:] = inImage + else : + images[i,:,:]=inImage + + affines.append(affine) + if i > 20 and early_stop: + break + + if getAffines: + return images, affines + else : + return images \ No newline at end of file From bfd5313e86b408b068c3c9e208166c7ce36f5a1f Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Tue, 22 Oct 2024 17:05:45 +1000 Subject: [PATCH 04/28] topic-recognition - check if cude is available (returns true) --- recognition/2d_unet_s46974426/train.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index e69de29bb..ee25da693 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -0,0 +1,2 @@ +import torch +print(torch.cuda.is_available()) \ No newline at end of file From ce888100b35eceddee49eb27c97dd36e6c722c60 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Tue, 22 Oct 2024 17:53:30 +1000 Subject: [PATCH 05/28] topic-recognition - quick test to load and display one of the slices and the result --- recognition/2d_unet_s46974426/Figure_1.png | Bin 0 -> 9858 bytes recognition/2d_unet_s46974426/test.py | 38 +++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 recognition/2d_unet_s46974426/Figure_1.png create mode 100644 recognition/2d_unet_s46974426/test.py diff --git a/recognition/2d_unet_s46974426/Figure_1.png b/recognition/2d_unet_s46974426/Figure_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b01fac9b7424ccec92a8aa85aeb8fb5dc2f002e3 GIT binary patch literal 9858 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 Date: Tue, 22 Oct 2024 17:59:40 +1000 Subject: [PATCH 06/28] topic-recognition - title and reference to initial slice image added to README report --- recognition/2d_unet_s46974426/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index 95ea2c6a0..142511130 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -1 +1,9 @@ -README file for Report 2D UNet report \ No newline at end of file +Using 2D UNet to segment the HipMRI Study on Prostate Cancer dataset + +Task Description from Blackboard: "Segment the HipMRI Study on Prostate Cancer (see Appendix for link) +using the processed 2D slices (2D images) available here with the 2D UNet [1] with all labels having a +minimum Dice similarity coefficient of 0.75 on the test set on the prostate label. You will need to load +Nifti file format and sample code is provided in Appendix B. [Easy Difficulty]" + +An initial test code was run to just visualise one of the slices before using 2D UNet to get a sense of +what the images look like. The resuling image afer test.py was run can be seen in Figure_1.png. \ No newline at end of file From bfc81fcb4681469a5488f45bcad5339fd14cacfa Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 11:15:39 +1000 Subject: [PATCH 07/28] topic-recognition - tested image loading implementation and resize function added to handle images that are not 246*128 --- .../__pycache__/dataset.cpython-311.pyc | Bin 0 -> 3802 bytes recognition/2d_unet_s46974426/dataset.py | 58 ++++++++++-------- recognition/2d_unet_s46974426/train.py | 22 ++++++- 3 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc diff --git a/recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc b/recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..910264eb4a2d0e70f1804499e1fad61d2e46e2f2 GIT binary patch literal 3802 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 np.ndarray : channels = np . unique ( arr ) @@ -14,54 +18,56 @@ def to_channels ( arr : np.ndarray, dtype = np.uint8) -> np.ndarray : return res # load medical image functions -def load_data_2D(imageNames, normImage=False, categorical=False, dtype = np.float32, getAffines = False , early_stop = False) : +def load_data_2D(imageNames, normImage=False, categorical=False, dtype=np.float32, getAffines=False, early_stop=False): ''' - Load medical image data from names , cases list provided into a list for each . + Load medical image data from names , cases list provided into a list for each. - This function pre - allocates 4D arrays for conv2d to avoid excessive memory & - usage . + This function pre - allocates 4D arrays for conv2d to avoid excessive memory &usage. normImage : bool ( normalise the image 0.0 -1.0) early_stop : Stop loading pre - maturely , leaves arrays mostly empty , for quick & - loading and testing scripts . - ''' + loading and testing scripts. + ''' + affines = [] - - # get fixed size num = len(imageNames) - first_case = nib.load(imageNames[0]).get_fdata(caching = 'unchanged') + first_case = nib.load(imageNames[0]).get_fdata(caching='unchanged') if len(first_case.shape) == 3: - first_case = first_case [: ,: ,0] # sometimes extra dims , remove + first_case = first_case[:, :, 0] # remove extra dims if necessary if categorical: - first_case = to_channels (first_case, dtype = dtype) + first_case = to_channels(first_case, dtype=dtype) rows, cols, channels = first_case.shape - images = np.zeros((num, rows, cols, channels), dtype = dtype) - else : + images = np.zeros((num, rows, cols, channels), dtype=dtype) + else: rows, cols = first_case.shape - images = np.zeros((num , rows , cols), dtype = dtype) + images = np.zeros((num, rows, cols), dtype=dtype) for i, inName in enumerate(tqdm(imageNames)): niftiImage = nib.load(inName) - inImage = niftiImage.get_fdata(caching = 'unchanged') # read disk only + inImage = niftiImage.get_fdata(caching='unchanged') affine = niftiImage.affine if len(inImage.shape) == 3: - inImage = inImage [: ,: ,0] # sometimes extra dims in HipMRI_study data - inImage = inImage.astype(dtype) + inImage = inImage[:, :, 0] # remove extra dims if necessary + inImage = inImage.astype(dtype) + + # Resize the image if necessary + if inImage.shape != (rows, cols): + inImage = resize_image(inImage, (rows, cols)) + if normImage: - #~ inImage = inImage / np. linalg . norm ( inImage ) - #~ inImage = 255. * inImage / inImage . max () inImage = (inImage - inImage.mean()) / inImage.std() - if categorical : - inImage = utils.to_channels(inImage, dtype = dtype) - images [i ,: ,: ,:] = inImage - else : - images[i,:,:]=inImage - + + if categorical: + inImage = utils.to_channels(inImage, dtype=dtype) + images[i, :, :, :] = inImage + else: + images[i, :, :] = inImage + affines.append(affine) if i > 20 and early_stop: break if getAffines: return images, affines - else : + else: return images \ No newline at end of file diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index ee25da693..c45e3f840 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -1,2 +1,20 @@ -import torch -print(torch.cuda.is_available()) \ No newline at end of file +import os +import numpy as np +from dataset import load_data_2D + +# folder that contains slices +image_folder = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train' +image_filenames = [os.path.join(image_folder, f) for f in os.listdir(image_folder) if f.endswith('.nii.gz')] + +# test loading the images +images = load_data_2D(image_filenames, normImage=True, categorical=False, dtype=np.float32, early_stop=False) +images, affines = load_data_2D(image_filenames, normImage=True, getAffines=True) +print(f'Loaded {len(affines)} affines') + +import matplotlib.pyplot as plt + +# display one imaage to confirm loading was correct +plt.imshow(images[0, :, :], cmap='gray') +plt.title('First Image Slice') +plt.axis('off') +plt.show() \ No newline at end of file From 60eeea56de3d15364a09a213f7c29b00f2c4b8ca Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 11:49:55 +1000 Subject: [PATCH 08/28] topic-recognition - updated README report --- recognition/2d_unet_s46974426/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index 142511130..0529ef276 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -6,4 +6,8 @@ minimum Dice similarity coefficient of 0.75 on the test set on the prostate labe Nifti file format and sample code is provided in Appendix B. [Easy Difficulty]" An initial test code was run to just visualise one of the slices before using 2D UNet to get a sense of -what the images look like. The resuling image afer test.py was run can be seen in Figure_1.png. \ No newline at end of file +what the images look like. The resuling image afer test.py was run can be seen in Figure_1.png. + +The data loader was run in a simple for to check that it worked, it was ~50% successful when it errorred due +to image sizing issue. To resolve this, an image resizing function was added to be called by the data +loader. \ No newline at end of file From 6772a7db51a830c1498e0c7b8d79155d2b7fed7b Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 12:10:24 +1000 Subject: [PATCH 09/28] topic-recognition - added first cut of UNet initialisation to modules.py --- recognition/2d_unet_s46974426/modules.py | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index e69de29bb..aa631078b 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -0,0 +1,70 @@ +import torch +import torch.nn as nn + +''' +PyTorch UNet initialiser to be called in other python scripts +Initialisation done for common medical image segmentation +''' +class UNet(nn.Module): + def __init__(self, in_channels=1, out_channels=1): + super(UNet, self).__init__() + # reduce spatial dimensions, increases channels + # aims to learn more complex features at higher resolutions + self.enc1 = self.contract_block(in_channels, 64) + self.enc2 = self.contract_block(64, 128) + self.enc3 = self.contract_block(128, 256) + self.enc4 = self.contract_block(256, 512) + + # deepest abstract level of UNet (bottleneck) + self.bottleneck = self.contract_block(512, 1024) + + # decoder to mirror contractin path above + self.upconv4 = self.expand_block(1024, 512) + self.upconv3 = self.expand_block(512, 256) + self.upconv2 = self.expand_block(256, 128) + self.upconv1 = self.expand_block(128, 64) + + # final layer kernal size of 1 because binary segmentation + self.final = nn.Conv2d(64, out_channels, kernel_size=1) + + def __call__(self, x): + # contracting path + enc1 = self.enc1(x) + enc2 = self.enc2(enc1) + enc3 = self.enc3(enc2) + enc4 = self.enc4(enc3) + + # bottleneck + bottleneck = self.bottleneck(enc4) + + # expanding path + upconv4 = self.upconv4(bottleneck, enc4) + upconv3 = self.upconv3(upconv4, enc3) + upconv2 = self.upconv2(upconv3, enc2) + upconv1 = self.upconv1(upconv2, enc1) + + return torch.sigmoid(self.final(upconv1)) + + ''' + UNet encoder - reduces the spatial dimensions while increasing the number of feature channels + ''' + def contract_block(self, in_channels, out_channels, kernel_size=3): + return nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size), + nn.ReLU(), + nn.Conv2d(out_channels, out_channels, kernel_size), + nn.ReLU(), + nn.MaxPool2d(2) + ) + + ''' + UNet decoder - upsamples the feature maps back to the original image dimensions while reducing the number of channels + ''' + def expand_block(self, in_channels, out_channels, kernel_size=3): + return nn.Sequential( + nn.ConvTranspose2d(in_channels, out_channels, kernel_size), + nn.ReLU(), + nn.Conv2d(out_channels, out_channels, kernel_size), + nn.ReLU() + ) + From bb8db339cb8443a91dd30b5de74347608ee2ba59 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 12:11:11 +1000 Subject: [PATCH 10/28] topic-recognition - added first cut of dice_loss function to modules.py --- recognition/2d_unet_s46974426/modules.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index aa631078b..3f744ec38 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -68,3 +68,11 @@ def expand_block(self, in_channels, out_channels, kernel_size=3): nn.ReLU() ) +def dice_loss(preds, targets, smooth=1e-6): + preds_flat = preds.view(-1) + targets_flat = targets.view(-1) + + intersection = (preds_flat * targets_flat).sum() + dice_loss_value = 1 - (2.0 * intersection + smooth) / (preds_flat.sum() + targets_flat.sum() + smooth) + + return dice_loss_value From e1da81debbecc42b4776161d9a6e4d29d01015e3 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 12:19:07 +1000 Subject: [PATCH 11/28] topic-recognition - dice_loss function comments added --- recognition/2d_unet_s46974426/modules.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index 3f744ec38..ff56120e6 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -68,6 +68,14 @@ def expand_block(self, in_channels, out_channels, kernel_size=3): nn.ReLU() ) +''' +Measure the overlap between predicted segmentation masks and ground truth masks + +parameters: +preds = predicted segmentation mask +targets = actual segmentation mask +smooth = constant to prevent division by 0 +''' def dice_loss(preds, targets, smooth=1e-6): preds_flat = preds.view(-1) targets_flat = targets.view(-1) From ed12eee2e0e069cf5e8c0813403c8a0555ace227 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 12:23:02 +1000 Subject: [PATCH 12/28] topic-recognition - Figure_1.png renamed to be more meaningfull and created images folder --- .../images/data_loader_test.png | Bin 0 -> 15942 bytes .../slice_print_from_initial_test.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 recognition/2d_unet_s46974426/images/data_loader_test.png rename recognition/2d_unet_s46974426/{Figure_1.png => images/slice_print_from_initial_test.png} (100%) diff --git a/recognition/2d_unet_s46974426/images/data_loader_test.png b/recognition/2d_unet_s46974426/images/data_loader_test.png new file mode 100644 index 0000000000000000000000000000000000000000..46be4f5af6cedd3a075f3754a21f57d0a4901a62 GIT binary patch literal 15942 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 Twa5MFf2iNr`Kws@;fwzRZ)vM! literal 0 HcmV?d00001 diff --git a/recognition/2d_unet_s46974426/Figure_1.png b/recognition/2d_unet_s46974426/images/slice_print_from_initial_test.png similarity index 100% rename from recognition/2d_unet_s46974426/Figure_1.png rename to recognition/2d_unet_s46974426/images/slice_print_from_initial_test.png From d523966fb5c3dc0f28921d34e984619b92029089 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 12:33:36 +1000 Subject: [PATCH 13/28] topic-recognition - first cut of training functionality added --- recognition/2d_unet_s46974426/train.py | 60 +++++++++++++++++++------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index c45e3f840..04dc8c089 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -1,20 +1,50 @@ +import torch +import torch.optim as optim +import torch.nn as nn +from torch.utils.data import DataLoader +from modules import UNet +from utils import load_data_2D +from modules import dice_loss import os -import numpy as np -from dataset import load_data_2D -# folder that contains slices -image_folder = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train' -image_filenames = [os.path.join(image_folder, f) for f in os.listdir(image_folder) if f.endswith('.nii.gz')] +# paths to the train, test, validation data +train_dir = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train' +val_dir = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_validate' -# test loading the images -images = load_data_2D(image_filenames, normImage=True, categorical=False, dtype=np.float32, early_stop=False) -images, affines = load_data_2D(image_filenames, normImage=True, getAffines=True) -print(f'Loaded {len(affines)} affines') +# hyperparameters +batch_size = 8 +epochs = 50 +learning_rate = 1e-4 # 0.0001 +device = 'cuda' if torch.cuda.is_available() else 'cpu' +print(device) -import matplotlib.pyplot as plt +# load training data +train_images = load_data_2D([os.path.join(train_dir, f) for f in os.listdir(train_dir)]) +train_loader = DataLoader(train_images, batch_size=batch_size, shuffle=True) -# display one imaage to confirm loading was correct -plt.imshow(images[0, :, :], cmap='gray') -plt.title('First Image Slice') -plt.axis('off') -plt.show() \ No newline at end of file +# initialize model +model = UNet().to(device) +optimizer = optim.Adam(model.parameters(), lr=learning_rate) +criterion = nn.BCELoss() # Binary Cross-Entropy loss for segmentation + +# training loop +for epoch in range(epochs): + model.train() + running_loss = 0.0 + for i, data in enumerate(train_loader): + inputs, labels = data['image'].to(device), data['label'].to(device) + + optimizer.zero_grad() + outputs = model(inputs) + + # Calculate both BCE and Dice loss + bce_loss = criterion(outputs, labels) + dice = dice_loss(outputs, labels) + loss = bce_loss + dice + + loss.backward() + optimizer.step() + + running_loss += loss.item() + + print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(train_loader)}") From a0f5f401f1eaa18ccf1064d680f485abe1d908e6 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 18:26:28 +1000 Subject: [PATCH 14/28] topic-recognition - changes to README report --- recognition/2d_unet_s46974426/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index 0529ef276..6c9637d43 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -6,8 +6,9 @@ minimum Dice similarity coefficient of 0.75 on the test set on the prostate labe Nifti file format and sample code is provided in Appendix B. [Easy Difficulty]" An initial test code was run to just visualise one of the slices before using 2D UNet to get a sense of -what the images look like. The resuling image afer test.py was run can be seen in Figure_1.png. +what the images look like. The resuling image afer test.py was run can be seen in slice_print_from_initial_test +in the images folder. The data loader was run in a simple for to check that it worked, it was ~50% successful when it errorred due to image sizing issue. To resolve this, an image resizing function was added to be called by the data -loader. \ No newline at end of file +loader. The completed data_loader test output can be seen in data_loader_test.png in the images folder. \ No newline at end of file From 405a93849955ac6d7baa381eb90565b82e059a3d Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 18:31:17 +1000 Subject: [PATCH 15/28] topic-recognition - added accuracy and loss tracking to plot accuracy and loss after training complete --- recognition/2d_unet_s46974426/train.py | 109 +++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 8 deletions(-) diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 04dc8c089..aeb558827 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -6,38 +6,53 @@ from utils import load_data_2D from modules import dice_loss import os +import matplotlib.pyplot as plt +from sklearn.metrics import confusion_matrix +import seaborn as sns +import numpy as np -# paths to the train, test, validation data +# paths to the train, validation data train_dir = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train' val_dir = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_validate' -# hyperparameters +# set hyperparams below batch_size = 8 epochs = 50 -learning_rate = 1e-4 # 0.0001 +learning_rate = 1e-4 # 0.0001 device = 'cuda' if torch.cuda.is_available() else 'cpu' print(device) -# load training data +# load training and validation data using the load_data_2D function train_images = load_data_2D([os.path.join(train_dir, f) for f in os.listdir(train_dir)]) +val_images = load_data_2D([os.path.join(val_dir, f) for f in os.listdir(val_dir)]) train_loader = DataLoader(train_images, batch_size=batch_size, shuffle=True) +val_loader = DataLoader(val_images, batch_size=batch_size, shuffle=False) -# initialize model +# Initialize model model = UNet().to(device) optimizer = optim.Adam(model.parameters(), lr=learning_rate) -criterion = nn.BCELoss() # Binary Cross-Entropy loss for segmentation +# binary cross-entropy loss for segmentation +criterion = nn.BCELoss() + +# lists to store losses and accuracy so they can be plotted later +train_losses = [] +val_losses = [] +train_dice_scores = [] +val_dice_scores = [] # training loop for epoch in range(epochs): model.train() running_loss = 0.0 + dice_score = 0.0 + for i, data in enumerate(train_loader): inputs, labels = data['image'].to(device), data['label'].to(device) optimizer.zero_grad() outputs = model(inputs) - # Calculate both BCE and Dice loss + # calculate both BCE and dice loss (using import from modules) bce_loss = criterion(outputs, labels) dice = dice_loss(outputs, labels) loss = bce_loss + dice @@ -46,5 +61,83 @@ optimizer.step() running_loss += loss.item() + dice_score += 1 - dice.item() - print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(train_loader)}") + avg_train_loss = running_loss / len(train_loader) + avg_train_dice = dice_score / len(train_loader) + train_losses.append(avg_train_loss) + train_dice_scores.append(avg_train_dice) + + # validation step using validation data + model.eval() + val_running_loss = 0.0 + val_dice_score = 0.0 + with torch.no_grad(): + for i, data in enumerate(val_loader): + inputs, labels = data['image'].to(device), data['label'].to(device) + outputs = model(inputs) + + bce_loss = criterion(outputs, labels) + dice = dice_loss(outputs, labels) + loss = bce_loss + dice + + val_running_loss += loss.item() + val_dice_score += 1 - dice.item() + + avg_val_loss = val_running_loss / len(val_loader) + avg_val_dice = val_dice_score / len(val_loader) + val_losses.append(avg_val_loss) + val_dice_scores.append(avg_val_dice) + + print(f"Epoch {epoch+1}/{epochs}, Train Loss: {avg_train_loss}, Val Loss: {avg_val_loss}, Train Dice: {avg_train_dice}, Val Dice: {avg_val_dice}") + +# plot the training and validation loss +plt.figure() +plt.plot(train_losses, label='Train Loss') +plt.plot(val_losses, label='Validation Loss') +plt.xlabel('Epochs') +plt.ylabel('Loss') +plt.legend() +plt.title('Training and Validation Loss') +plt.show() + +# plot the dice similarity coefficient (accuracy) for train and validation +plt.figure() +plt.plot(train_dice_scores, label='Train Dice Score') +plt.plot(val_dice_scores, label='Validation Dice Score') +plt.xlabel('Epochs') +plt.ylabel('Dice Score') +plt.legend() +plt.title('Training and Validation Dice Score') +plt.show() + +# create confusion matrix +all_preds = [] +all_labels = [] + +model.eval() +with torch.no_grad(): + for i, data in enumerate(val_loader): + inputs, labels = data['image'].to(device), data['label'].to(device) + outputs = model(inputs) + + # threshold the outputs to binary values (0 or 1) + preds = (outputs > 0.5).float() + + all_preds.append(preds.cpu().view(-1).numpy()) + all_labels.append(labels.cpu().view(-1).numpy()) + +# flatten all predictions and labels +all_preds = np.concatenate(all_preds) +all_labels = np.concatenate(all_labels) + +# confusion matrix +cm = confusion_matrix(all_labels, all_preds) + +# plot confusion matrix +plt.figure(figsize=(6, 6)) +sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Background', 'Prostate'], yticklabels=['Background', 'Prostate']) +plt.xlabel('Predicted') +plt.ylabel('Actual') +plt.title('Confusion Matrix') +plt.show() From aa87eea247e6059790a0a7a73986959d085deaad Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 18:33:38 +1000 Subject: [PATCH 16/28] topic-recognition - added trained model saving after for use by test.py --- recognition/2d_unet_s46974426/train.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index aeb558827..13b281a66 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -111,6 +111,9 @@ plt.title('Training and Validation Dice Score') plt.show() +# save the trained model for later use by test.py +torch.save(model.state_dict(), 'unet_prostate.pth') + # create confusion matrix all_preds = [] all_labels = [] From 12bc644683545fc7685208b745625cca66a214be Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Wed, 23 Oct 2024 23:25:37 +1000 Subject: [PATCH 17/28] topic-recognition - remove unecessary test.py file and some code added --- .../__pycache__/dataset.cpython-311.pyc | Bin 3802 -> 6706 bytes .../__pycache__/modules.cpython-311.pyc | Bin 0 -> 3714 bytes recognition/2d_unet_s46974426/dataset.py | 70 +++++-- recognition/2d_unet_s46974426/modules.py | 110 ++++------- recognition/2d_unet_s46974426/predict.py | 32 ++++ recognition/2d_unet_s46974426/test.py | 38 ---- recognition/2d_unet_s46974426/train.py | 174 +++++------------- 7 files changed, 163 insertions(+), 261 deletions(-) create mode 100644 recognition/2d_unet_s46974426/__pycache__/modules.cpython-311.pyc delete mode 100644 recognition/2d_unet_s46974426/test.py diff --git a/recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc b/recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc index 910264eb4a2d0e70f1804499e1fad61d2e46e2f2..956f5b72cf8b7f801cc0ad738fe578a405ce1ed5 100644 GIT binary patch literal 6706 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 delta 1165 zcmZ`&-)kd99N)?ACfm(!a&0eZ@1%6RJDbL9`djqmY7eiJDtIXI#UjS-?j+rFf26as z;i4^#P$fbiQdXpj4=4ERzy~o8mg62o?hi;H$gxuK_1+@Bh#)w#UcK|dFU)7=`};Gq z-~Dbe97}zrsxn8V`O;`*MLtN4g7*({^A&!NTTdWxU1B~aWnc(YANWV(I756?D+@+^ zPh6Lcgu$|s|3*>0EE>`tSXT^C$!1r|b)?*{$yDP-oT9x~^*F^7VK2qg_4^;~Uxd3L zIsAHz<95`Y6ybw$KGXg&{w7FIknxjvj=L@fIq5ZUDtK2~%4W6&ENm;=@p7!lZ7cMt zBn^R}C*=VQ{*vZ)U)?l4SoR&An63k>7{LP6EX&n3f;_mm0yT`aIt_6#)Lo}GZ$RQg zt>&5rL^pKgd1ehk8`&;!OvcWbWx9`i%EaQ^*hEQM!e^V{U{3 zOAH4ph?L5PMl?J^UD3c`7K^iim%NZ=;}!}w(wva@h!KiDF)c6nGC2c=gP)SmgR{XI zW$H?fhIyz_;QHC%J!KIb2e*}Wd_)}jjT;F( z^(kt&rG7m4STCFVI`OUF$u4!XOTFw;@N#Nrmkm{34MoSdtM!O+n4s6vHjstSQy*sY zg)cHlFo-T9`ldskp@By9zR_*NM456U3-b}HTU<1*5$Qjr%eHIy7J3d}r6mAe-uD#x zJOIJ@Ggof(g&5EP{WBh-#1&?~S?hBNkdELm_Ng2lj)v}>eQ187fK6rTODy)he>6w;bB5E7CWt0E*0;-{kgG$~Q}Nl6G7%hk$wH{jr`&FrRy z)G0Y2HA1P1DpiRP6*;u2QF7qOkt0VhT8U^SB&4c`+=Adxaq4?(@7kNhL~1)u-oAPB z-ZwLE-n=(|)Ypdzw9o9wm12;PKe6MsK$&?x1k5Z^h{9w@y!2(p*f`6Scy5eM0YApa z0}NqFKT+7*MB!9+6c9^ofg@xQ{!1+$QkaV*%CBH|AXSz+A6Msf47xwStSX5!3W>8y zaGH!TfLs^n6z(>O^9m0%pag)z*bOEg0?d%jUm8%26$Z>xE*K)?X0ZQSUY6R6S}LokwAisQLz zJ4JQudTgQ0fnAyFwpmTHgS{|IOtHfwX)T$N(KgE4ysl@8_cPF+oEuvFa^v*Sjk zS0~GIs^?tkOiEs$Vm*xkT?*=P&de?(RxlNE!xg z{1i-dQ%~#rV_mU>2?{H`0y|D;wM0yj$2HZE^@E3w9zJ+5b|^8HRmL-_-kG~e@dzgB z@e+2rKEfE(Eg*kSkrzV!kILOXQ`l(9QhS)k3at(TsFkk!}>eY})am)7Kq$S8G(V3- zpGBgMIMQQ9P>iUXiK!WhS&HxYaq?+HQ9NskXD#vUTCG=gfovv|2gXNZZ|H1k*G#o)-U0T6;+)g`ywBm6)?%ocPi3Z2r)w#(zm*X&2akwS!p8KwF?n$zEVX!C;nc|Qo z4y|)$fJ0{jStXq7OfSv>50ir9%s5DAG5LdFeaka`M@+Jy>BW1szTp)}Cm+3 zI?Pci>1z-3v<@3d45RkpXU9kk@;2J-LY9NR5+=j>@K=z8e4>`03S*U?5(ey zrmostDrwM0l)jGy-yP99CxzL}4V#DB-|f(tLwg(mn};&q<4_O-->svwA<31Rlj`iy zNS1z^q{@Wo+`n?K4HR_(u|7qXrR_gW-kF>mG^G|xYMJKCf?o&kCkpM4PZf6^H+LPk zfJ!GU>BQ#aFB^Bx%0J7$5B`>T)c)jDvE#JaaoPgf*l#uVPlsQYCyblYK1&~q?!<5=B3Eu@yCWcIhSE_V` z0zSaBx#JpogRciiP#9aIEC7xOvh%9zC!t%4F;Vh{R3#t@z?+W literal 0 HcmV?d00001 diff --git a/recognition/2d_unet_s46974426/dataset.py b/recognition/2d_unet_s46974426/dataset.py index 02a3cb584..ee7bc7cf6 100644 --- a/recognition/2d_unet_s46974426/dataset.py +++ b/recognition/2d_unet_s46974426/dataset.py @@ -3,37 +3,44 @@ from tqdm import tqdm import utils as utils import cv2 +import os # Ensure you have this to work with file paths +import torch +from torch.utils.data import Dataset # Add this import for Dataset def resize_image(image, target_shape): """Resize image to the target shape using OpenCV.""" return cv2.resize(image, (target_shape[1], target_shape[0]), interpolation=cv2.INTER_LINEAR) -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 c in channels : +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 c in channels: c = int(c) - res [... , c : c + 1][arr == c] = 1 - + res[..., c:c + 1][arr == c] = 1 return res -# load medical image functions def load_data_2D(imageNames, normImage=False, categorical=False, dtype=np.float32, getAffines=False, early_stop=False): - ''' - Load medical image data from names , cases list provided into a list for each. + """ + Load medical image data from names, cases list provided into a list for each. - This function pre - allocates 4D arrays for conv2d to avoid excessive memory &usage. + Parameters: + - imageNames: List of image file names + - normImage: bool (normalize the image 0.0 - 1.0) + - categorical: bool (indicates if the data is categorical) + - dtype: Desired data type (default: np.float32) + - getAffines: bool (return affine matrices along with images) + - early_stop: bool (stop loading prematurely for testing purposes) - normImage : bool ( normalise the image 0.0 -1.0) - early_stop : Stop loading pre - maturely , leaves arrays mostly empty , for quick & - loading and testing scripts. - ''' - + Returns: + - images: Loaded image data as a numpy array + - affines: List of affine matrices (if getAffines is True) + """ affines = [] num = len(imageNames) first_case = nib.load(imageNames[0]).get_fdata(caching='unchanged') + if len(first_case.shape) == 3: - first_case = first_case[:, :, 0] # remove extra dims if necessary + first_case = first_case[:, :, 0] # Remove extra dims if necessary if categorical: first_case = to_channels(first_case, dtype=dtype) rows, cols, channels = first_case.shape @@ -46,8 +53,9 @@ def load_data_2D(imageNames, normImage=False, categorical=False, dtype=np.float3 niftiImage = nib.load(inName) inImage = niftiImage.get_fdata(caching='unchanged') affine = niftiImage.affine + if len(inImage.shape) == 3: - inImage = inImage[:, :, 0] # remove extra dims if necessary + inImage = inImage[:, :, 0] # Remove extra dims if necessary inImage = inImage.astype(dtype) # Resize the image if necessary @@ -58,7 +66,7 @@ def load_data_2D(imageNames, normImage=False, categorical=False, dtype=np.float3 inImage = (inImage - inImage.mean()) / inImage.std() if categorical: - inImage = utils.to_channels(inImage, dtype=dtype) + inImage = to_channels(inImage, dtype=dtype) images[i, :, :, :] = inImage else: images[i, :, :] = inImage @@ -70,4 +78,28 @@ def load_data_2D(imageNames, normImage=False, categorical=False, dtype=np.float3 if getAffines: return images, affines else: - return images \ No newline at end of file + return images + +class MedicalImageDataset(torch.utils.data.Dataset): + def __init__(self, image_dir, label_dir, device): + self.image_filenames = sorted([os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith('.nii.gz')]) + self.label_filenames = sorted([os.path.join(label_dir, f) for f in os.listdir(label_dir) if f.endswith('.nii.gz')]) + self.normImage = True # Adjust as needed + self.categorical = False # Adjust as needed + self.device = device + + def __getitem__(self, idx): + print(f"Loading image: {self.image_filenames[idx]}") # Debug print + # Load image and label using your provided function + image = load_data_2D([self.image_filenames[idx]], normImage=self.normImage, categorical=self.categorical) + label = load_data_2D([self.label_filenames[idx]], normImage=False, categorical=self.categorical) + + # Convert to PyTorch tensors and move to the correct device + image = torch.tensor(image, dtype=torch.float32).to(self.device) # Ensure float type + label = torch.tensor(label, dtype=torch.float32).to(self.device) # Ensure float type + + return image, label + + def __len__(self): + return len(self.image_filenames) + diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index ff56120e6..78096ad97 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -1,86 +1,48 @@ import torch import torch.nn as nn +import torch.nn.functional as F -''' -PyTorch UNet initialiser to be called in other python scripts -Initialisation done for common medical image segmentation -''' class UNet(nn.Module): def __init__(self, in_channels=1, out_channels=1): super(UNet, self).__init__() - # reduce spatial dimensions, increases channels - # aims to learn more complex features at higher resolutions - self.enc1 = self.contract_block(in_channels, 64) - self.enc2 = self.contract_block(64, 128) - self.enc3 = self.contract_block(128, 256) - self.enc4 = self.contract_block(256, 512) - - # deepest abstract level of UNet (bottleneck) - self.bottleneck = self.contract_block(512, 1024) - - # decoder to mirror contractin path above - self.upconv4 = self.expand_block(1024, 512) - self.upconv3 = self.expand_block(512, 256) - self.upconv2 = self.expand_block(256, 128) - self.upconv1 = self.expand_block(128, 64) - - # final layer kernal size of 1 because binary segmentation - self.final = nn.Conv2d(64, out_channels, kernel_size=1) - - def __call__(self, x): - # contracting path - enc1 = self.enc1(x) - enc2 = self.enc2(enc1) - enc3 = self.enc3(enc2) - enc4 = self.enc4(enc3) - - # bottleneck - bottleneck = self.bottleneck(enc4) - - # expanding path - upconv4 = self.upconv4(bottleneck, enc4) - upconv3 = self.upconv3(upconv4, enc3) - upconv2 = self.upconv2(upconv3, enc2) - upconv1 = self.upconv1(upconv2, enc1) - - return torch.sigmoid(self.final(upconv1)) - - ''' - UNet encoder - reduces the spatial dimensions while increasing the number of feature channels - ''' - def contract_block(self, in_channels, out_channels, kernel_size=3): + self.encoder1 = self.conv_block(in_channels, 64) + self.encoder2 = self.conv_block(64, 128) + self.encoder3 = self.conv_block(128, 256) + + self.bottleneck = self.conv_block(256, 512) + + self.decoder3 = self.upconv_block(512, 256) + self.decoder2 = self.upconv_block(256, 128) + self.decoder1 = self.upconv_block(128, 64) + + self.final_conv = nn.Conv2d(64, out_channels, kernel_size=1) + + def conv_block(self, in_channels, out_channels): return nn.Sequential( - nn.Conv2d(in_channels, out_channels, kernel_size), - nn.ReLU(), - nn.Conv2d(out_channels, out_channels, kernel_size), - nn.ReLU(), - nn.MaxPool2d(2) + nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1), + nn.ReLU(inplace=True), + nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + nn.ReLU(inplace=True) ) - - ''' - UNet decoder - upsamples the feature maps back to the original image dimensions while reducing the number of channels - ''' - def expand_block(self, in_channels, out_channels, kernel_size=3): + + def upconv_block(self, in_channels, out_channels): return nn.Sequential( - nn.ConvTranspose2d(in_channels, out_channels, kernel_size), - nn.ReLU(), - nn.Conv2d(out_channels, out_channels, kernel_size), - nn.ReLU() + nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2), + nn.ReLU(inplace=True) ) -''' -Measure the overlap between predicted segmentation masks and ground truth masks + def forward(self, x): + enc1 = self.encoder1(x) + enc2 = self.encoder2(F.max_pool2d(enc1, kernel_size=2)) + enc3 = self.encoder3(F.max_pool2d(enc2, kernel_size=2)) + + bottleneck = self.bottleneck(F.max_pool2d(enc3, kernel_size=2)) + + dec3 = self.decoder3(bottleneck) + dec3 = torch.cat((dec3, enc3), dim=1) + dec2 = self.decoder2(dec3) + dec2 = torch.cat((dec2, enc2), dim=1) + dec1 = self.decoder1(dec2) + dec1 = torch.cat((dec1, enc1), dim=1) -parameters: -preds = predicted segmentation mask -targets = actual segmentation mask -smooth = constant to prevent division by 0 -''' -def dice_loss(preds, targets, smooth=1e-6): - preds_flat = preds.view(-1) - targets_flat = targets.view(-1) - - intersection = (preds_flat * targets_flat).sum() - dice_loss_value = 1 - (2.0 * intersection + smooth) / (preds_flat.sum() + targets_flat.sum() + smooth) - - return dice_loss_value + return self.final_conv(dec1) diff --git a/recognition/2d_unet_s46974426/predict.py b/recognition/2d_unet_s46974426/predict.py index e69de29bb..9ed6f535d 100644 --- a/recognition/2d_unet_s46974426/predict.py +++ b/recognition/2d_unet_s46974426/predict.py @@ -0,0 +1,32 @@ +import torch +import numpy as np +import matplotlib.pyplot as plt +from modules import UNet +from dataset import load_data_2D # Ensure to adjust this based on your structure + +def predict(model, image): + model.eval() + with torch.no_grad(): + output = model(image.unsqueeze(0)) + return torch.sigmoid(output).squeeze(0) + +if __name__ == "__main__": + model = UNet(in_channels=1, out_channels=1) + model.load_state_dict(torch.load('model_checkpoint.pth')) # Load your trained model + + # Load a sample image for prediction + sample_image_paths = ["path/to/sample/image.nii.gz"] + sample_images = load_data_2D(sample_image_paths) + + preds = predict(model, sample_images[0]) # Assuming sample_images is the first loaded sample + + # Visualization + plt.subplot(1, 2, 1) + plt.title('Input Image') + plt.imshow(sample_images[0][0, :, :], cmap='gray') # Assuming single channel + + plt.subplot(1, 2, 2) + plt.title('Predicted Mask') + plt.imshow(preds.numpy(), cmap='gray') + + plt.show() diff --git a/recognition/2d_unet_s46974426/test.py b/recognition/2d_unet_s46974426/test.py deleted file mode 100644 index 14df9f46c..000000000 --- a/recognition/2d_unet_s46974426/test.py +++ /dev/null @@ -1,38 +0,0 @@ -import nibabel as nib -import numpy as np -import matplotlib.pyplot as plt - -# Load NIfTI file -nifti_file_path = 'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train/seg_004_week_0_slice_0.nii.gz' -nifti_image = nib.load(nifti_file_path) -image_data = nifti_image.get_fdata() # Get the image data as a NumPy array - -# Check the shape of the loaded image -print(f'Image shape: {image_data.shape}') - -# Display the image based on the shape -def display_image(image_data): - if len(image_data.shape) == 2: # If the image is 2D - plt.imshow(image_data, cmap='gray') - plt.title('Single 2D Slice') - plt.axis('off') - plt.show() - elif len(image_data.shape) == 3: # If the image is 3D - num_images = image_data.shape[2] - num_slices = min(num_images, 5) # Ensure we don't exceed available slices - slice_indices = np.linspace(0, num_images - 1, num_slices, dtype=int) - - plt.figure(figsize=(15, 5)) - for i, slice_idx in enumerate(slice_indices): - plt.subplot(1, num_slices, i + 1) - plt.imshow(image_data[:, :, slice_idx], cmap='gray') # Display the axial slice - plt.title(f'Slice {slice_idx}') - plt.axis('off') # Hide the axis - - plt.tight_layout() - plt.show() - else: - print("Unsupported image dimensions") - -# Call the function to display the image -display_image(image_data) diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 13b281a66..d5545dc41 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -1,146 +1,60 @@ +import os import torch -import torch.optim as optim import torch.nn as nn +import torch.optim as optim from torch.utils.data import DataLoader -from modules import UNet -from utils import load_data_2D -from modules import dice_loss -import os -import matplotlib.pyplot as plt -from sklearn.metrics import confusion_matrix -import seaborn as sns -import numpy as np - -# paths to the train, validation data -train_dir = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train' -val_dir = r'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_validate' - -# set hyperparams below -batch_size = 8 -epochs = 50 -learning_rate = 1e-4 # 0.0001 -device = 'cuda' if torch.cuda.is_available() else 'cpu' -print(device) - -# load training and validation data using the load_data_2D function -train_images = load_data_2D([os.path.join(train_dir, f) for f in os.listdir(train_dir)]) -val_images = load_data_2D([os.path.join(val_dir, f) for f in os.listdir(val_dir)]) -train_loader = DataLoader(train_images, batch_size=batch_size, shuffle=True) -val_loader = DataLoader(val_images, batch_size=batch_size, shuffle=False) - -# Initialize model -model = UNet().to(device) -optimizer = optim.Adam(model.parameters(), lr=learning_rate) -# binary cross-entropy loss for segmentation -criterion = nn.BCELoss() - -# lists to store losses and accuracy so they can be plotted later -train_losses = [] -val_losses = [] -train_dice_scores = [] -val_dice_scores = [] +from dataset import MedicalImageDataset # Make sure this import matches your dataset module +from modules import UNet # Import your U-Net model -# training loop -for epoch in range(epochs): - model.train() - running_loss = 0.0 - dice_score = 0.0 - - for i, data in enumerate(train_loader): - inputs, labels = data['image'].to(device), data['label'].to(device) - - optimizer.zero_grad() - outputs = model(inputs) - - # calculate both BCE and dice loss (using import from modules) - bce_loss = criterion(outputs, labels) - dice = dice_loss(outputs, labels) - loss = bce_loss + dice - - loss.backward() - optimizer.step() - - running_loss += loss.item() - dice_score += 1 - dice.item() - - avg_train_loss = running_loss / len(train_loader) - avg_train_dice = dice_score / len(train_loader) - train_losses.append(avg_train_loss) - train_dice_scores.append(avg_train_dice) +# Set device +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +print(f"CUDA Available: {torch.cuda.is_available()}") - # validation step using validation data - model.eval() - val_running_loss = 0.0 - val_dice_score = 0.0 - with torch.no_grad(): - for i, data in enumerate(val_loader): - inputs, labels = data['image'].to(device), data['label'].to(device) - outputs = model(inputs) - - bce_loss = criterion(outputs, labels) - dice = dice_loss(outputs, labels) - loss = bce_loss + dice +# Paths to your dataset +train_image_dir = 'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train' # Adjust this path +train_label_dir = 'C:/Users/rober/Desktop/COMP3710/keras_slices_train' # Adjust this path - val_running_loss += loss.item() - val_dice_score += 1 - dice.item() +# Load dataset +train_dataset = MedicalImageDataset(train_image_dir, train_label_dir, device) +train_loader = DataLoader(train_dataset, batch_size=1, shuffle=True) - avg_val_loss = val_running_loss / len(val_loader) - avg_val_dice = val_dice_score / len(val_loader) - val_losses.append(avg_val_loss) - val_dice_scores.append(avg_val_dice) +# Initialize model, optimizer, and loss function +in_channels = 1 # Adjust based on your input image channels +out_channels = 1 # Adjust based on the number of output channels +model = UNet(in_channels=in_channels, out_channels=out_channels).to(device) # Move model to device - print(f"Epoch {epoch+1}/{epochs}, Train Loss: {avg_train_loss}, Val Loss: {avg_val_loss}, Train Dice: {avg_train_dice}, Val Dice: {avg_val_dice}") +optimizer = optim.Adam(model.parameters(), lr=0.001) +criterion = nn.BCEWithLogitsLoss() # Assuming binary segmentation, adjust as needed -# plot the training and validation loss -plt.figure() -plt.plot(train_losses, label='Train Loss') -plt.plot(val_losses, label='Validation Loss') -plt.xlabel('Epochs') -plt.ylabel('Loss') -plt.legend() -plt.title('Training and Validation Loss') -plt.show() +# Training loop +num_epochs = 10 +for epoch in range(num_epochs): + model.train() # Set model to training mode + print(f'Starting epoch {epoch + 1}/{num_epochs}') -# plot the dice similarity coefficient (accuracy) for train and validation -plt.figure() -plt.plot(train_dice_scores, label='Train Dice Score') -plt.plot(val_dice_scores, label='Validation Dice Score') -plt.xlabel('Epochs') -plt.ylabel('Dice Score') -plt.legend() -plt.title('Training and Validation Dice Score') -plt.show() + for images, labels in train_loader: + try: + # Move images and labels to device + images, labels = images.to(device), labels.to(device) -# save the trained model for later use by test.py -torch.save(model.state_dict(), 'unet_prostate.pth') + # Debugging: Print shapes and devices + print(f"Images shape: {images.shape}, Labels shape: {labels.shape}") + print(f"Images device: {images.device}, Labels device: {labels.device}") -# create confusion matrix -all_preds = [] -all_labels = [] + # Forward pass + outputs = model(images) # Ensure both are on the same device -model.eval() -with torch.no_grad(): - for i, data in enumerate(val_loader): - inputs, labels = data['image'].to(device), data['label'].to(device) - outputs = model(inputs) - - # threshold the outputs to binary values (0 or 1) - preds = (outputs > 0.5).float() - - all_preds.append(preds.cpu().view(-1).numpy()) - all_labels.append(labels.cpu().view(-1).numpy()) + # Debugging: Print outputs shape + print(f"Outputs shape: {outputs.shape}") -# flatten all predictions and labels -all_preds = np.concatenate(all_preds) -all_labels = np.concatenate(all_labels) + # Calculate loss + loss = criterion(outputs, labels) -# confusion matrix -cm = confusion_matrix(all_labels, all_preds) + # Backward pass and optimization + optimizer.zero_grad() + loss.backward() + optimizer.step() -# plot confusion matrix -plt.figure(figsize=(6, 6)) -sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Background', 'Prostate'], yticklabels=['Background', 'Prostate']) -plt.xlabel('Predicted') -plt.ylabel('Actual') -plt.title('Confusion Matrix') -plt.show() + print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}') + except Exception as e: + print(f"Error during training: {e}") From 2356e79de94d20c3129709d4ba6af158ede4e8b7 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Thu, 24 Oct 2024 21:52:17 +1000 Subject: [PATCH 18/28] topic-recognition - first compilable and running version (ran on google colab so needs editing) --- recognition/2d_unet_s46974426/dataset.py | 36 +-- recognition/2d_unet_s46974426/modules.py | 207 +++++++++++--- recognition/2d_unet_s46974426/train.py | 328 +++++++++++++++++++---- 3 files changed, 451 insertions(+), 120 deletions(-) diff --git a/recognition/2d_unet_s46974426/dataset.py b/recognition/2d_unet_s46974426/dataset.py index ee7bc7cf6..c0346647b 100644 --- a/recognition/2d_unet_s46974426/dataset.py +++ b/recognition/2d_unet_s46974426/dataset.py @@ -1,7 +1,6 @@ import numpy as np import nibabel as nib -from tqdm import tqdm -import utils as utils +from tqdm import tqdm import cv2 import os # Ensure you have this to work with file paths import torch @@ -53,24 +52,24 @@ def load_data_2D(imageNames, normImage=False, categorical=False, dtype=np.float3 niftiImage = nib.load(inName) inImage = niftiImage.get_fdata(caching='unchanged') affine = niftiImage.affine - + if len(inImage.shape) == 3: inImage = inImage[:, :, 0] # Remove extra dims if necessary inImage = inImage.astype(dtype) - + # Resize the image if necessary if inImage.shape != (rows, cols): inImage = resize_image(inImage, (rows, cols)) - + if normImage: inImage = (inImage - inImage.mean()) / inImage.std() - + if categorical: inImage = to_channels(inImage, dtype=dtype) images[i, :, :, :] = inImage else: images[i, :, :] = inImage - + affines.append(affine) if i > 20 and early_stop: break @@ -80,26 +79,3 @@ def load_data_2D(imageNames, normImage=False, categorical=False, dtype=np.float3 else: return images -class MedicalImageDataset(torch.utils.data.Dataset): - def __init__(self, image_dir, label_dir, device): - self.image_filenames = sorted([os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith('.nii.gz')]) - self.label_filenames = sorted([os.path.join(label_dir, f) for f in os.listdir(label_dir) if f.endswith('.nii.gz')]) - self.normImage = True # Adjust as needed - self.categorical = False # Adjust as needed - self.device = device - - def __getitem__(self, idx): - print(f"Loading image: {self.image_filenames[idx]}") # Debug print - # Load image and label using your provided function - image = load_data_2D([self.image_filenames[idx]], normImage=self.normImage, categorical=self.categorical) - label = load_data_2D([self.label_filenames[idx]], normImage=False, categorical=self.categorical) - - # Convert to PyTorch tensors and move to the correct device - image = torch.tensor(image, dtype=torch.float32).to(self.device) # Ensure float type - label = torch.tensor(label, dtype=torch.float32).to(self.device) # Ensure float type - - return image, label - - def __len__(self): - return len(self.image_filenames) - diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index 78096ad97..a0b36cde8 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -1,48 +1,189 @@ +"""https://github.com/milesial/Pytorch-UNet/blob/master/unet/unet_model.py""" + +""" Parts of the U-Net model """ + import torch import torch.nn as nn import torch.nn.functional as F +from torch import Tensor +from torch.utils.data import Dataset -class UNet(nn.Module): - def __init__(self, in_channels=1, out_channels=1): - super(UNet, self).__init__() - self.encoder1 = self.conv_block(in_channels, 64) - self.encoder2 = self.conv_block(64, 128) - self.encoder3 = self.conv_block(128, 256) - - self.bottleneck = self.conv_block(256, 512) - - self.decoder3 = self.upconv_block(512, 256) - self.decoder2 = self.upconv_block(256, 128) - self.decoder1 = self.upconv_block(128, 64) +class DoubleConv(nn.Module): + """(convolution => [BN] => ReLU) * 2""" - self.final_conv = nn.Conv2d(64, out_channels, kernel_size=1) - - def conv_block(self, in_channels, out_channels): - return nn.Sequential( - nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1), + def __init__(self, in_channels, out_channels, mid_channels=None): + super().__init__() + if not mid_channels: + mid_channels = out_channels + self.double_conv = nn.Sequential( + nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False), + nn.BatchNorm2d(mid_channels), nn.ReLU(inplace=True), - nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False), + nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True) ) - def upconv_block(self, in_channels, out_channels): - return nn.Sequential( - nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2), - nn.ReLU(inplace=True) + 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.MaxPool2d(2), + DoubleConv(in_channels, out_channels) ) def forward(self, x): - enc1 = self.encoder1(x) - enc2 = self.encoder2(F.max_pool2d(enc1, kernel_size=2)) - enc3 = self.encoder3(F.max_pool2d(enc2, kernel_size=2)) + 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='bilinear', align_corners=True) + self.conv = DoubleConv(in_channels, out_channels, in_channels // 2) + else: + self.up = nn.ConvTranspose2d(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 + diffY = x2.size()[2] - x1.size()[2] + diffX = x2.size()[3] - x1.size()[3] + + x1 = F.pad(x1, [diffX // 2, diffX - diffX // 2, + diffY // 2, diffY - diffY // 2]) + # if you have padding issues, see + # https://github.com/HaiyongJiang/U-Net-Pytorch-Unstructured-Buggy/commit/0e854509c2cea854e247a9c615f175f76fbb2e3a + # https://github.com/xiaopeng-liao/Pytorch-UNet/commit/8ebac70e633bac59fc22bb5195e513d5832fb3bd + x = torch.cat([x2, x1], dim=1) + return self.conv(x) + + +class OutConv(nn.Module): + def __init__(self, in_channels, out_channels): + super(OutConv, self).__init__() + self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=1) + + def forward(self, x): + return self.conv(x) + + +class UNet(nn.Module): + def __init__(self, n_channels, n_classes, bilinear=False): + super(UNet, self).__init__() + self.n_channels = n_channels + self.n_classes = n_classes + self.bilinear = bilinear + + self.inc = (DoubleConv(n_channels, 64)) + self.down1 = (Down(64, 128)) + self.down2 = (Down(128, 256)) + self.down3 = (Down(256, 512)) + factor = 2 if bilinear else 1 + self.down4 = (Down(512, 1024 // factor)) + self.up1 = (Up(1024, 512 // factor, bilinear)) + self.up2 = (Up(512, 256 // factor, bilinear)) + self.up3 = (Up(256, 128 // factor, bilinear)) + self.up4 = (Up(128, 64, bilinear)) + self.outc = (OutConv(64, n_classes)) + + 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 + + def use_checkpointing(self): + self.inc = torch.utils.checkpoint(self.inc) + self.down1 = torch.utils.checkpoint(self.down1) + self.down2 = torch.utils.checkpoint(self.down2) + self.down3 = torch.utils.checkpoint(self.down3) + self.down4 = torch.utils.checkpoint(self.down4) + self.up1 = torch.utils.checkpoint(self.up1) + self.up2 = torch.utils.checkpoint(self.up2) + self.up3 = torch.utils.checkpoint(self.up3) + self.up4 = torch.utils.checkpoint(self.up4) + self.outc = torch.utils.checkpoint(self.outc) + +def dice_coeff(input: Tensor, target: Tensor, reduce_batch_first: bool = False, epsilon: float = 1e-6): + # Average of Dice coefficient for all batches, or for a single mask + assert input.size() == target.size() + assert input.dim() == 3 or not reduce_batch_first + + sum_dim = (-1, -2) if input.dim() == 2 or not reduce_batch_first else (-1, -2, -3) + + inter = 2 * (input * target).sum(dim=sum_dim) + sets_sum = input.sum(dim=sum_dim) + target.sum(dim=sum_dim) + sets_sum = torch.where(sets_sum == 0, inter, sets_sum) + + dice = (inter + epsilon) / (sets_sum + epsilon) + return dice.mean() + + +def multiclass_dice_coeff(input: Tensor, target: Tensor, reduce_batch_first: bool = False, epsilon: float = 1e-6): + # Average of Dice coefficient for all classes + return dice_coeff(input.flatten(0, 1), target.flatten(0, 1), reduce_batch_first, epsilon) + + +def dice_loss(input: Tensor, target: Tensor, multiclass: bool = False): + # Dice loss (objective to minimize) between 0 and 1 + fn = multiclass_dice_coeff if multiclass else dice_coeff + return 1 - fn(input, target, reduce_batch_first=True) + +class CombinedDataset(Dataset): + def __init__(self, images, image_masks, transform=None): + """ + Args: + images (numpy array): The array containing image data. + image_masks (numpy array): The array containing corresponding mask data. + transform (callable, optional): Optional transform to be applied on a sample. + """ + self.images = images + self.image_masks = image_masks + self.transform = transform + + def __len__(self): + # Return the number of samples in the dataset (should be the same for images and masks) + return len(self.images) + + def __getitem__(self, idx): + # Load the image and the corresponding mask + image = self.images[idx] + mask = self.image_masks[idx] + + # Add a channel dimension if the images are grayscale (i.e., single-channel) + if len(image.shape) == 2: # If the image is HxW (no channel dimension) + image = np.expand_dims(image, axis=0) # Add channel dimension -> (1, H, W) + + if len(mask.shape) == 2: # If the mask is HxW (no channel dimension) + mask = np.expand_dims(mask, axis=0) # Add channel dimension -> (1, H, W) - bottleneck = self.bottleneck(F.max_pool2d(enc3, kernel_size=2)) + # Apply any transformation if provided + if self.transform: + image = self.transform(image) + mask = self.transform(mask) - dec3 = self.decoder3(bottleneck) - dec3 = torch.cat((dec3, enc3), dim=1) - dec2 = self.decoder2(dec3) - dec2 = torch.cat((dec2, enc2), dim=1) - dec1 = self.decoder1(dec2) - dec1 = torch.cat((dec1, enc1), dim=1) + # Convert to PyTorch tensors + image = torch.from_numpy(image).float() + mask = torch.from_numpy(mask).float() - return self.final_conv(dec1) + return image, mask \ No newline at end of file diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index d5545dc41..6e4482c91 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -1,60 +1,274 @@ +import argparse +import logging import os +import random +import sys import torch import torch.nn as nn -import torch.optim as optim -from torch.utils.data import DataLoader -from dataset import MedicalImageDataset # Make sure this import matches your dataset module -from modules import UNet # Import your U-Net model - -# Set device -device = torch.device("cuda" if torch.cuda.is_available() else "cpu") -print(f"CUDA Available: {torch.cuda.is_available()}") - -# Paths to your dataset -train_image_dir = 'C:/Users/rober/Desktop/COMP3710/keras_slices_seg_train' # Adjust this path -train_label_dir = 'C:/Users/rober/Desktop/COMP3710/keras_slices_train' # Adjust this path - -# Load dataset -train_dataset = MedicalImageDataset(train_image_dir, train_label_dir, device) -train_loader = DataLoader(train_dataset, batch_size=1, shuffle=True) - -# Initialize model, optimizer, and loss function -in_channels = 1 # Adjust based on your input image channels -out_channels = 1 # Adjust based on the number of output channels -model = UNet(in_channels=in_channels, out_channels=out_channels).to(device) # Move model to device - -optimizer = optim.Adam(model.parameters(), lr=0.001) -criterion = nn.BCEWithLogitsLoss() # Assuming binary segmentation, adjust as needed - -# Training loop -num_epochs = 10 -for epoch in range(num_epochs): - model.train() # Set model to training mode - print(f'Starting epoch {epoch + 1}/{num_epochs}') - - for images, labels in train_loader: - try: - # Move images and labels to device - images, labels = images.to(device), labels.to(device) - - # Debugging: Print shapes and devices - print(f"Images shape: {images.shape}, Labels shape: {labels.shape}") - print(f"Images device: {images.device}, Labels device: {labels.device}") - - # Forward pass - outputs = model(images) # Ensure both are on the same device - - # Debugging: Print outputs shape - print(f"Outputs shape: {outputs.shape}") - - # Calculate loss - loss = criterion(outputs, labels) - - # Backward pass and optimization - optimizer.zero_grad() - loss.backward() - optimizer.step() - - print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}') - except Exception as e: - print(f"Error during training: {e}") +import torch.nn.functional as F +import torchvision.transforms as transforms +import torchvision.transforms.functional as TF +from pathlib import Path +from torch import optim +from torch.utils.data import DataLoader, random_split +from tqdm import tqdm + +import wandb + +dir_img = Path('/content/train2') +dir_mask = Path('/content/train2_seg') +dir_checkpoint = Path('/content/checkpoints') + +def evaluate(net, dataloader, device, amp): + net.eval() + num_val_batches = len(dataloader) + dice_score = 0 + + # iterate over the validation set + with torch.autocast(device.type if device.type != 'mps' else 'cpu', enabled=amp): + for batch in tqdm(dataloader, total=num_val_batches, desc='Validation round', unit='batch', leave=False): + image, mask_true = batch['image'], batch['mask'] + + # move images and labels to correct device and type + image = image.to(device=device, dtype=torch.float32, memory_format=torch.channels_last) + mask_true = mask_true.to(device=device, dtype=torch.long) + + # predict the mask + mask_pred = net(image) + + if net.n_classes == 1: + assert mask_true.min() >= 0 and mask_true.max() <= 1, 'True mask indices should be in [0, 1]' + mask_pred = (F.sigmoid(mask_pred) > 0.5).float() + # compute the Dice score + dice_score += dice_coeff(mask_pred, mask_true, reduce_batch_first=False) + else: + assert mask_true.min() >= 0 and mask_true.max() < net.n_classes, 'True mask indices should be in [0, n_classes[' + # convert to one-hot format + mask_true = F.one_hot(mask_true, net.n_classes).permute(0, 3, 1, 2).float() + mask_pred = F.one_hot(mask_pred.argmax(dim=1), net.n_classes).permute(0, 3, 1, 2).float() + # compute the Dice score, ignoring background + dice_score += multiclass_dice_coeff(mask_pred[:, 1:], mask_true[:, 1:], reduce_batch_first=False) + + net.train() + return dice_score / max(num_val_batches, 1) + +def train_model( + model, + device, + epochs: int = 5, + batch_size: int = 1, + learning_rate: float = 1e-5, + val_percent: float = 0.1, + save_checkpoint: bool = True, + img_scale: float = 0.5, + amp: bool = False, + weight_decay: float = 1e-8, + momentum: float = 0.999, + gradient_clipping: float = 1.0, +): + image_files = [os.path.join(dir_img, f) for f in os.listdir(dir_img) if f.endswith('.nii.gz') or f.endswith('.nii')] + # Step 2: Load the images using load_data_2D + images = load_data_2D(image_files, normImage=False, categorical=False, dtype=np.float32, getAffines=False, early_stop=False) + + image_files_mask = [os.path.join(dir_mask, f) for f in os.listdir(dir_mask) if f.endswith('.nii.gz') or f.endswith('.nii')] + # Step 2: Load the images using load_data_2D + images_mask = load_data_2D(image_files_mask, normImage=False, categorical=False, dtype=np.float32, getAffines=False, early_stop=False) + + + training_set = CombinedDataset(images, images_mask) + # 2. Split into train / validation partitions + n_val = int(len(images) * val_percent) + n_train = len(images) - n_val + train_set, val_set = random_split(training_set, [n_train, n_val], generator=torch.Generator().manual_seed(0)) + print(len(train_set)) + + train_loader = DataLoader(train_set, shuffle=True) + val_loader = DataLoader(val_set, shuffle=False, drop_last=True) + + # (Initialize logging) + # experiment = wandb.init(project='U-Net', resume='allow', anonymous='must') + # experiment.config.update( + # dict(epochs=epochs, batch_size=batch_size, learning_rate=learning_rate, + # val_percent=val_percent, save_checkpoint=save_checkpoint, img_scale=img_scale, amp=amp) + # ) + + logging.info(f'''Starting training: + Epochs: {epochs} + Batch size: {batch_size} + Learning rate: {learning_rate} + Training size: {n_train} + Validation size: {n_val} + Checkpoints: {save_checkpoint} + Device: {device.type} + Images scaling: {img_scale} + Mixed Precision: {amp} + ''') + + # 4. Set up the optimizer, the loss, the learning rate scheduler and the loss scaling for AMP + optimizer = optim.RMSprop(model.parameters(), + lr=learning_rate, weight_decay=weight_decay, momentum=momentum, foreach=True) + scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'max', patience=5) # goal: maximize Dice score + grad_scaler = torch.cuda.amp.GradScaler(enabled=amp) + criterion = nn.CrossEntropyLoss() if model.n_classes > 1 else nn.BCEWithLogitsLoss() + global_step = 0 + + # 5. Begin training + for epoch in range(1, epochs + 1): + model.train() + epoch_loss = 0 + with tqdm(total=n_train, desc=f'Epoch {epoch}/{epochs}', unit='img') as pbar: + for batch in train_loader: + + images, true_masks = batch + #print("masks ", true_masks.size()) + true_masks = true_masks.squeeze(1) + true_masks = torch.clamp(true_masks, min=0, max=1) + + #print("hello: ", images.size()) + + assert images.shape[1] == model.n_channels, \ + f'Network has been defined with {model.n_channels} input channels, ' \ + f'but loaded images have {images.shape[1]} channels. Please check that ' \ + 'the images are loaded correctly.' + + images = images.to(device=device, dtype=torch.float32, memory_format=torch.channels_last) + true_masks = true_masks.to(device=device, dtype=torch.long) + + with torch.autocast(device.type if device.type != 'mps' else 'cpu', enabled=amp): + masks_pred = model(images) + if model.n_classes == 1: + loss = criterion(masks_pred.squeeze(1), true_masks.float()) + loss += dice_loss(F.sigmoid(masks_pred.squeeze(1)), true_masks.float(), multiclass=False) + else: + loss = criterion(masks_pred, true_masks) + loss += dice_loss( + F.softmax(masks_pred, dim=1).float(), + F.one_hot(true_masks, model.n_classes).permute(0, 3, 1, 2).float(), + multiclass=True + ) + + optimizer.zero_grad(set_to_none=True) + grad_scaler.scale(loss).backward() + grad_scaler.unscale_(optimizer) + torch.nn.utils.clip_grad_norm_(model.parameters(), gradient_clipping) + grad_scaler.step(optimizer) + grad_scaler.update() + + pbar.update(images.shape[0]) + global_step += 1 + epoch_loss += loss.item() + # experiment.log({ + # 'train loss': loss.item(), + # 'step': global_step, + # 'epoch': epoch + # }) + pbar.set_postfix(**{'loss (batch)': loss.item()}) + + # Evaluation round + division_step = (n_train // (5 * batch_size)) + if division_step > 0: + if global_step % division_step == 0: + histograms = {} + for tag, value in model.named_parameters(): + tag = tag.replace('/', '.') + if not (torch.isinf(value) | torch.isnan(value)).any(): + histograms['Weights/' + tag] = wandb.Histogram(value.data.cpu()) + if not (torch.isinf(value.grad) | torch.isnan(value.grad)).any(): + histograms['Gradients/' + tag] = wandb.Histogram(value.grad.data.cpu()) + + val_score = evaluate(model, val_loader, device, amp) + scheduler.step(val_score) + + logging.info('Validation Dice score: {}'.format(val_score)) + try: + experiment.log({ + 'learning rate': optimizer.param_groups[0]['lr'], + 'validation Dice': val_score, + 'images': wandb.Image(images[0].cpu()), + 'masks': { + 'true': wandb.Image(true_masks[0].float().cpu()), + 'pred': wandb.Image(masks_pred.argmax(dim=1)[0].float().cpu()), + }, + 'step': global_step, + 'epoch': epoch, + **histograms + }) + except: + pass + + if save_checkpoint: + Path(dir_checkpoint).mkdir(parents=True, exist_ok=True) + state_dict = model.state_dict() + state_dict['mask_values'] = dataset.mask_values + torch.save(state_dict, str(dir_checkpoint / 'checkpoint_epoch{}.pth'.format(epoch))) + logging.info(f'Checkpoint {epoch} saved!') + + +def get_args(): + parser = argparse.ArgumentParser(description='Train the UNet on images and target masks') + parser.add_argument('--epochs', '-e', metavar='E', type=int, default=5, help='Number of epochs') + parser.add_argument('--batch-size', '-b', dest='batch_size', metavar='B', type=int, default=1, help='Batch size') + parser.add_argument('--learning-rate', '-l', metavar='LR', type=float, default=1e-5, + help='Learning rate', dest='lr') + parser.add_argument('--load', '-f', type=str, default=False, help='Load model from a .pth file') + parser.add_argument('--scale', '-s', type=float, default=0.5, help='Downscaling factor of the images') + parser.add_argument('--validation', '-v', dest='val', type=float, default=10.0, + help='Percent of the data that is used as validation (0-100)') + parser.add_argument('--amp', action='store_true', default=False, help='Use mixed precision') + parser.add_argument('--bilinear', action='store_true', default=False, help='Use bilinear upsampling') + parser.add_argument('--classes', '-c', type=int, default=2, help='Number of classes') + + return parser.parse_args() + +args = get_args() +logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +logging.info(f'Using device {device}') + +# Change here to adapt to your data +# n_channels=3 for RGB images +# n_classes is the number of probabilities you want to get per pixel +model = UNet(n_channels=1, n_classes=2, bilinear=args.bilinear) +print("hi: ", args.classes) +model = model.to(memory_format=torch.channels_last) + +logging.info(f'Network:\n' + f'\t{model.n_channels} input channels\n' + f'\t{model.n_classes} output channels (classes)\n' + f'\t{"Bilinear" if model.bilinear else "Transposed conv"} upscaling') + +# if args.load: +# state_dict = torch.load(args.load, map_location=device) +# del state_dict['mask_values'] +# model.load_state_dict(state_dict) +# logging.info(f'Model loaded from {args.load}') + +model.to(device=device) +try: + train_model( + model=model, + epochs=args.epochs, + batch_size=args.batch_size, + learning_rate=args.lr, + device=device, + img_scale=args.scale, + val_percent=args.val / 100, + amp=args.amp + ) +except torch.cuda.OutOfMemoryError: + logging.error('Detected OutOfMemoryError! ' + 'Enabling checkpointing to reduce memory usage, but this slows down training. ' + 'Consider enabling AMP (--amp) for fast and memory efficient training') + torch.cuda.empty_cache() + model.use_checkpointing() + train_model( + model=model, + epochs=args.epochs, + batch_size=args.batch_size, + learning_rate=args.lr, + device=device, + img_scale=args.scale, + val_percent=args.val / 100, + amp=args.amp + ) + From 43fe33ec34d204ba1e9de22c85f77998014ac454 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Thu, 24 Oct 2024 21:58:39 +1000 Subject: [PATCH 19/28] topic-recognition - cleared warning/errors by referencing each file and import correctly --- recognition/2d_unet_s46974426/modules.py | 1 + recognition/2d_unet_s46974426/train.py | 30 ++++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index a0b36cde8..b2a4ccc43 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -7,6 +7,7 @@ import torch.nn.functional as F from torch import Tensor from torch.utils.data import Dataset +import numpy as np class DoubleConv(nn.Module): """(convolution => [BN] => ReLU) * 2""" diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 6e4482c91..899ed6b87 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -12,6 +12,9 @@ from torch import optim from torch.utils.data import DataLoader, random_split from tqdm import tqdm +from modules import dice_coeff, multiclass_dice_coeff, UNet, CombinedDataset, dice_loss +from dataset import load_data_2D +import numpy as np import wandb @@ -181,25 +184,26 @@ def train_model( logging.info('Validation Dice score: {}'.format(val_score)) try: - experiment.log({ - 'learning rate': optimizer.param_groups[0]['lr'], - 'validation Dice': val_score, - 'images': wandb.Image(images[0].cpu()), - 'masks': { - 'true': wandb.Image(true_masks[0].float().cpu()), - 'pred': wandb.Image(masks_pred.argmax(dim=1)[0].float().cpu()), - }, - 'step': global_step, - 'epoch': epoch, - **histograms - }) + pass + # experiment.log({ + # 'learning rate': optimizer.param_groups[0]['lr'], + # 'validation Dice': val_score, + # 'images': wandb.Image(images[0].cpu()), + # 'masks': { + # 'true': wandb.Image(true_masks[0].float().cpu()), + # 'pred': wandb.Image(masks_pred.argmax(dim=1)[0].float().cpu()), + # }, + # 'step': global_step, + # 'epoch': epoch, + # **histograms + # }) except: pass if save_checkpoint: Path(dir_checkpoint).mkdir(parents=True, exist_ok=True) state_dict = model.state_dict() - state_dict['mask_values'] = dataset.mask_values + state_dict['mask_values'] = training_set.mask_values torch.save(state_dict, str(dir_checkpoint / 'checkpoint_epoch{}.pth'.format(epoch))) logging.info(f'Checkpoint {epoch} saved!') From 4d4d7f271c84b94a8110d4c591b9fb1a74c94448 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Thu, 24 Oct 2024 22:04:24 +1000 Subject: [PATCH 20/28] topic-recognition - fixed data paths and updated readme report --- recognition/2d_unet_s46974426/README.md | 4 ++++ recognition/2d_unet_s46974426/train.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index 6c9637d43..be6024ea1 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -5,6 +5,10 @@ using the processed 2D slices (2D images) available here with the 2D UNet [1] wi minimum Dice similarity coefficient of 0.75 on the test set on the prostate label. You will need to load Nifti file format and sample code is provided in Appendix B. [Easy Difficulty]" +I quickly want to mention that I prefixed each commit with 'topic recognition' this was a force of habit, +typically when working on git repositories I branch the solution to a branch named after a change request +e.g. "CR-123" and prefix each commit with this. + An initial test code was run to just visualise one of the slices before using 2D UNet to get a sense of what the images look like. The resuling image afer test.py was run can be seen in slice_print_from_initial_test in the images folder. diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 899ed6b87..6745e106b 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -18,9 +18,9 @@ import wandb -dir_img = Path('/content/train2') -dir_mask = Path('/content/train2_seg') -dir_checkpoint = Path('/content/checkpoints') +dir_img = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_test') +dir_mask = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_seg_test') +dir_checkpoint = Path('./checkpoints') def evaluate(net, dataloader, device, amp): net.eval() From dd140abce0087bd984557bd604e2049ef2fed873 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 10:48:14 +1000 Subject: [PATCH 21/28] topic-recognition - first successful run (need to add validation data instead of data split) --- recognition/2d_unet_s46974426/train.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 6745e106b..18bbfcc9d 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -28,13 +28,15 @@ def evaluate(net, dataloader, device, amp): dice_score = 0 # iterate over the validation set - with torch.autocast(device.type if device.type != 'mps' else 'cpu', enabled=amp): + with torch.autocast(device_type = 'cuda'): for batch in tqdm(dataloader, total=num_val_batches, desc='Validation round', unit='batch', leave=False): - image, mask_true = batch['image'], batch['mask'] + image, mask_true = batch # move images and labels to correct device and type image = image.to(device=device, dtype=torch.float32, memory_format=torch.channels_last) mask_true = mask_true.to(device=device, dtype=torch.long) + mask_true = mask_true.squeeze(1) + mask_true = torch.clamp(mask_true, min=0, max=1) # predict the mask mask_pred = net(image) @@ -203,7 +205,7 @@ def train_model( if save_checkpoint: Path(dir_checkpoint).mkdir(parents=True, exist_ok=True) state_dict = model.state_dict() - state_dict['mask_values'] = training_set.mask_values + state_dict['mask_values'] = training_set.image_masks torch.save(state_dict, str(dir_checkpoint / 'checkpoint_epoch{}.pth'.format(epoch))) logging.info(f'Checkpoint {epoch} saved!') From 28610a25851ad931e04df1d93606a2e8cce1d4ca Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 10:53:56 +1000 Subject: [PATCH 22/28] topic-recognition - validation data added instead of data split --- .../__pycache__/dataset.cpython-311.pyc | Bin 6706 -> 0 bytes .../__pycache__/modules.cpython-311.pyc | Bin 3714 -> 0 bytes recognition/2d_unet_s46974426/train.py | 13 ++++++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) delete mode 100644 recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc delete mode 100644 recognition/2d_unet_s46974426/__pycache__/modules.cpython-311.pyc diff --git a/recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc b/recognition/2d_unet_s46974426/__pycache__/dataset.cpython-311.pyc deleted file mode 100644 index 956f5b72cf8b7f801cc0ad738fe578a405ce1ed5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6706 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 diff --git a/recognition/2d_unet_s46974426/__pycache__/modules.cpython-311.pyc b/recognition/2d_unet_s46974426/__pycache__/modules.cpython-311.pyc deleted file mode 100644 index 0d8c4bc90f62dbad9c6c60f63fe829c499e08a63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3714 zcmb_eO>7fK6rTODy)he>6w;bB5E7CWt0E*0;-{kgG$~Q}Nl6G7%hk$wH{jr`&FrRy z)G0Y2HA1P1DpiRP6*;u2QF7qOkt0VhT8U^SB&4c`+=Adxaq4?(@7kNhL~1)u-oAPB z-ZwLE-n=(|)Ypdzw9o9wm12;PKe6MsK$&?x1k5Z^h{9w@y!2(p*f`6Scy5eM0YApa z0}NqFKT+7*MB!9+6c9^ofg@xQ{!1+$QkaV*%CBH|AXSz+A6Msf47xwStSX5!3W>8y zaGH!TfLs^n6z(>O^9m0%pag)z*bOEg0?d%jUm8%26$Z>xE*K)?X0ZQSUY6R6S}LokwAisQLz zJ4JQudTgQ0fnAyFwpmTHgS{|IOtHfwX)T$N(KgE4ysl@8_cPF+oEuvFa^v*Sjk zS0~GIs^?tkOiEs$Vm*xkT?*=P&de?(RxlNE!xg z{1i-dQ%~#rV_mU>2?{H`0y|D;wM0yj$2HZE^@E3w9zJ+5b|^8HRmL-_-kG~e@dzgB z@e+2rKEfE(Eg*kSkrzV!kILOXQ`l(9QhS)k3at(TsFkk!}>eY})am)7Kq$S8G(V3- zpGBgMIMQQ9P>iUXiK!WhS&HxYaq?+HQ9NskXD#vUTCG=gfovv|2gXNZZ|H1k*G#o)-U0T6;+)g`ywBm6)?%ocPi3Z2r)w#(zm*X&2akwS!p8KwF?n$zEVX!C;nc|Qo z4y|)$fJ0{jStXq7OfSv>50ir9%s5DAG5LdFeaka`M@+Jy>BW1szTp)}Cm+3 zI?Pci>1z-3v<@3d45RkpXU9kk@;2J-LY9NR5+=j>@K=z8e4>`03S*U?5(ey zrmostDrwM0l)jGy-yP99CxzL}4V#DB-|f(tLwg(mn};&q<4_O-->svwA<31Rlj`iy zNS1z^q{@Wo+`n?K4HR_(u|7qXrR_gW-kF>mG^G|xYMJKCf?o&kCkpM4PZf6^H+LPk zfJ!GU>BQ#aFB^Bx%0J7$5B`>T)c)jDvE#JaaoPgf*l#uVPlsQYCyblYK1&~q?!<5=B3Eu@yCWcIhSE_V` z0zSaBx#JpogRciiP#9aIEC7xOvh%9zC!t%4F;Vh{R3#t@z?+W diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 18bbfcc9d..533d276ec 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -20,6 +20,8 @@ dir_img = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_test') dir_mask = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_seg_test') +dir_img_val = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_validate') +dir_mask_val = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_seg_validate') dir_checkpoint = Path('./checkpoints') def evaluate(net, dataloader, device, amp): @@ -79,12 +81,21 @@ def train_model( # Step 2: Load the images using load_data_2D images_mask = load_data_2D(image_files_mask, normImage=False, categorical=False, dtype=np.float32, getAffines=False, early_stop=False) + image_files_val = [os.path.join(dir_img_val, f) for f in os.listdir(dir_img_val) if f.endswith('.nii.gz') or f.endswith('.nii')] + # Step 2: Load the images using load_data_2D + images_val = load_data_2D(image_files_val, normImage=False, categorical=False, dtype=np.float32, getAffines=False, early_stop=False) + + image_files_mask_val = [os.path.join(dir_mask_val, f) for f in os.listdir(dir_mask_val) if f.endswith('.nii.gz') or f.endswith('.nii')] + # Step 2: Load the images using load_data_2D + images_mask_val = load_data_2D(image_files_mask_val, normImage=False, categorical=False, dtype=np.float32, getAffines=False, early_stop=False) + training_set = CombinedDataset(images, images_mask) + validate_set = CombinedDataset(images_val, images_mask_val) # 2. Split into train / validation partitions n_val = int(len(images) * val_percent) n_train = len(images) - n_val - train_set, val_set = random_split(training_set, [n_train, n_val], generator=torch.Generator().manual_seed(0)) + train_set, val_set = training_set, validate_set print(len(train_set)) train_loader = DataLoader(train_set, shuffle=True) From cf38b5ba23b3b81863fda3b4540220f6aefa91a4 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 15:41:06 +1000 Subject: [PATCH 23/28] topic-recognition - updates to train script and report --- recognition/2d_unet_s46974426/README.md | 23 +++- .../images/50Epoc_validation_score.png | Bin 0 -> 57456 bytes .../images/50Epoch_batch_loss.png | Bin 0 -> 26787 bytes .../images/5Epochs_batch_loss.png | Bin 0 -> 31183 bytes .../images/5Epochs_dice_score.png | Bin 0 -> 32383 bytes recognition/2d_unet_s46974426/modules.py | 126 +++++++++++++++++- recognition/2d_unet_s46974426/train.py | 70 ++++++++-- 7 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 recognition/2d_unet_s46974426/images/50Epoc_validation_score.png create mode 100644 recognition/2d_unet_s46974426/images/50Epoch_batch_loss.png create mode 100644 recognition/2d_unet_s46974426/images/5Epochs_batch_loss.png create mode 100644 recognition/2d_unet_s46974426/images/5Epochs_dice_score.png diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index be6024ea1..98b536bd6 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -15,4 +15,25 @@ in the images folder. The data loader was run in a simple for to check that it worked, it was ~50% successful when it errorred due to image sizing issue. To resolve this, an image resizing function was added to be called by the data -loader. The completed data_loader test output can be seen in data_loader_test.png in the images folder. \ No newline at end of file +loader. The completed data_loader test output can be seen in data_loader_test.png in the images folder. + +After messing around with fixing errors from the original versions of modules, dataset, predict and train I +eventually gave up on those versions as they would not run. + +I went online and found a similar example of a 2d UNet implemented using pytorch and adapted the code to suit +my problem and reference to this can be seen below. + +Author: milesial +Date: 11/02/2024 +Title of program/source code: U-Net: Semantic segmentation with PyTorch +Code version: 475 +Type (e.g. computer program, source code): computer program +Web address or publisher (e.g. program publisher, URL): https://github.com/milesial/Pytorch-UNet + +Also during this process I discovered that the masks were inb the segment datasets and the images were in the +datasets not suffixed with 'seg' (I had it the wrong way around originally). + +I ran the train code just for the first 5 epochs and a graph showing the batch loss and a graph showing the dice +score can both be seen in the images folder. + +I then ran it for 50 epochs and the graphs similar to above are in the images folder. \ No newline at end of file diff --git a/recognition/2d_unet_s46974426/images/50Epoc_validation_score.png b/recognition/2d_unet_s46974426/images/50Epoc_validation_score.png new file mode 100644 index 0000000000000000000000000000000000000000..a30b776890d2156eb9662819f60d533f18d5f43c GIT binary patch literal 57456 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 literal 0 HcmV?d00001 diff --git a/recognition/2d_unet_s46974426/images/50Epoch_batch_loss.png b/recognition/2d_unet_s46974426/images/50Epoch_batch_loss.png new file mode 100644 index 0000000000000000000000000000000000000000..70f7824c1f2e2743e266c3a73b51c067ec5d2d86 GIT binary patch literal 26787 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 MjjRmG$DObK7lsF7H2?qr literal 0 HcmV?d00001 diff --git a/recognition/2d_unet_s46974426/images/5Epochs_batch_loss.png b/recognition/2d_unet_s46974426/images/5Epochs_batch_loss.png new file mode 100644 index 0000000000000000000000000000000000000000..79d949eba598621c0a41912aba332981ba0cf811 GIT binary patch literal 31183 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 literal 0 HcmV?d00001 diff --git a/recognition/2d_unet_s46974426/images/5Epochs_dice_score.png b/recognition/2d_unet_s46974426/images/5Epochs_dice_score.png new file mode 100644 index 0000000000000000000000000000000000000000..b77376528587c084e29744571bfd5e075dbf37fe GIT binary patch literal 32383 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 literal 0 HcmV?d00001 diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index b2a4ccc43..aba86d710 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -2,12 +2,21 @@ """ Parts of the U-Net model """ +import logging import torch import torch.nn as nn import torch.nn.functional as F from torch import Tensor from torch.utils.data import Dataset import numpy as np +from pathlib import Path +from PIL import Image +from os.path import splitext, isfile, join +from os import listdir +from tqdm import tqdm +from functools import partial +from multiprocessing import Pool +import matplotlib.pyplot as plt class DoubleConv(nn.Module): """(convolution => [BN] => ReLU) * 2""" @@ -187,4 +196,119 @@ def __getitem__(self, idx): image = torch.from_numpy(image).float() mask = torch.from_numpy(mask).float() - return image, mask \ No newline at end of file + return image, mask + +def load_image(filename): + ext = splitext(filename)[1] + if ext == '.npy': + return Image.fromarray(np.load(filename)) + elif ext in ['.pt', '.pth']: + return Image.fromarray(torch.load(filename).numpy()) + else: + return Image.open(filename) + + +def unique_mask_values(idx, mask_dir, mask_suffix): + mask_file = list(mask_dir.glob(idx + mask_suffix + '.*'))[0] + mask = np.asarray(load_image(mask_file)) + if mask.ndim == 2: + return np.unique(mask) + elif mask.ndim == 3: + mask = mask.reshape(-1, mask.shape[-1]) + return np.unique(mask, axis=0) + else: + raise ValueError(f'Loaded masks should have 2 or 3 dimensions, found {mask.ndim}') + + +class BasicDataset(Dataset): + def __init__(self, images_dir: str, mask_dir: str, scale: float = 1.0, mask_suffix: str = ''): + self.images_dir = Path(images_dir) + self.mask_dir = Path(mask_dir) + assert 0 < scale <= 1, 'Scale must be between 0 and 1' + self.scale = scale + self.mask_suffix = mask_suffix + + self.ids = [splitext(file)[0] for file in listdir(images_dir) if isfile(join(images_dir, file)) and not file.startswith('.')] + if not self.ids: + raise RuntimeError(f'No input file found in {images_dir}, make sure you put your images there') + + logging.info(f'Creating dataset with {len(self.ids)} examples') + logging.info('Scanning mask files to determine unique values') + with Pool() as p: + unique = list(tqdm( + p.imap(partial(unique_mask_values, mask_dir=self.mask_dir, mask_suffix=self.mask_suffix), self.ids), + total=len(self.ids) + )) + + self.mask_values = list(sorted(np.unique(np.concatenate(unique), axis=0).tolist())) + logging.info(f'Unique mask values: {self.mask_values}') + + def __len__(self): + return len(self.ids) + + @staticmethod + def preprocess(mask_values, pil_img, scale, is_mask): + w, h = pil_img.size + newW, newH = int(scale * w), int(scale * h) + assert newW > 0 and newH > 0, 'Scale is too small, resized images would have no pixel' + pil_img = pil_img.resize((newW, newH), resample=Image.NEAREST if is_mask else Image.BICUBIC) + img = np.asarray(pil_img) + + if is_mask: + mask = np.zeros((newH, newW), dtype=np.int64) + for i, v in enumerate(mask_values): + if img.ndim == 2: + mask[img == v] = i + else: + mask[(img == v).all(-1)] = i + + return mask + + else: + if img.ndim == 2: + img = img[np.newaxis, ...] + else: + img = img.transpose((2, 0, 1)) + + if (img > 1).any(): + img = img / 255.0 + + return img + + def __getitem__(self, idx): + name = self.ids[idx] + mask_file = list(self.mask_dir.glob(name + self.mask_suffix + '.*')) + img_file = list(self.images_dir.glob(name + '.*')) + + assert len(img_file) == 1, f'Either no image or multiple images found for the ID {name}: {img_file}' + assert len(mask_file) == 1, f'Either no mask or multiple masks found for the ID {name}: {mask_file}' + mask = load_image(mask_file[0]) + img = load_image(img_file[0]) + + assert img.size == mask.size, \ + f'Image and mask {name} should be the same size, but are {img.size} and {mask.size}' + + img = self.preprocess(self.mask_values, img, self.scale, is_mask=False) + mask = self.preprocess(self.mask_values, mask, self.scale, is_mask=True) + + return { + 'image': torch.as_tensor(img.copy()).float().contiguous(), + 'mask': torch.as_tensor(mask.copy()).long().contiguous() + } + + +class CarvanaDataset(BasicDataset): + def __init__(self, images_dir, mask_dir, scale=1): + super().__init__(images_dir, mask_dir, scale, mask_suffix='_mask') + + +def plot_img_and_mask(img, mask): + classes = mask.max() + 1 + fig, ax = plt.subplots(1, classes + 1) + ax[0].set_title('Input image') + ax[0].imshow(img) + for i in range(classes): + ax[i + 1].set_title(f'Mask (class {i + 1})') + ax[i + 1].imshow(mask == i) + plt.xticks([]), plt.yticks([]) + plt.show() \ No newline at end of file diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 533d276ec..431137dd0 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -15,6 +15,9 @@ from modules import dice_coeff, multiclass_dice_coeff, UNet, CombinedDataset, dice_loss from dataset import load_data_2D import numpy as np +import matplotlib.pyplot as plt +from sklearn.metrics import confusion_matrix +import seaborn as sns import wandb @@ -24,6 +27,10 @@ dir_mask_val = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_seg_validate') dir_checkpoint = Path('./checkpoints') +batch_losses = [] +val_dice_scores = [] +conf_matrix_total = None + def evaluate(net, dataloader, device, amp): net.eval() num_val_batches = len(dataloader) @@ -33,36 +40,44 @@ def evaluate(net, dataloader, device, amp): with torch.autocast(device_type = 'cuda'): for batch in tqdm(dataloader, total=num_val_batches, desc='Validation round', unit='batch', leave=False): image, mask_true = batch - - # move images and labels to correct device and type image = image.to(device=device, dtype=torch.float32, memory_format=torch.channels_last) - mask_true = mask_true.to(device=device, dtype=torch.long) - mask_true = mask_true.squeeze(1) + mask_true = mask_true.to(device=device, dtype=torch.long).squeeze(1) mask_true = torch.clamp(mask_true, min=0, max=1) - # predict the mask mask_pred = net(image) if net.n_classes == 1: - assert mask_true.min() >= 0 and mask_true.max() <= 1, 'True mask indices should be in [0, 1]' mask_pred = (F.sigmoid(mask_pred) > 0.5).float() - # compute the Dice score dice_score += dice_coeff(mask_pred, mask_true, reduce_batch_first=False) + + # Confusion matrix for binary classification + # preds_flat = mask_pred.view(-1).cpu().numpy() + # labels_flat = mask_true.view(-1).cpu().numpy() + # conf_matrix = confusion_matrix(labels_flat, preds_flat) + else: - assert mask_true.min() >= 0 and mask_true.max() < net.n_classes, 'True mask indices should be in [0, n_classes[' - # convert to one-hot format mask_true = F.one_hot(mask_true, net.n_classes).permute(0, 3, 1, 2).float() mask_pred = F.one_hot(mask_pred.argmax(dim=1), net.n_classes).permute(0, 3, 1, 2).float() - # compute the Dice score, ignoring background dice_score += multiclass_dice_coeff(mask_pred[:, 1:], mask_true[:, 1:], reduce_batch_first=False) + # Confusion matrix for multi-class classification + # preds_flat = mask_pred.argmax(dim=1).view(-1).cpu().numpy() + # labels_flat = mask_true.argmax(dim=1).view(-1).cpu().numpy() + # conf_matrix = confusion_matrix(labels_flat, preds_flat) + + # if conf_matrix_total is None: + # conf_matrix_total = conf_matrix + # else: + # conf_matrix_total += conf_matrix + net.train() + return dice_score / max(num_val_batches, 1) def train_model( model, device, - epochs: int = 5, + epochs: int = 50, batch_size: int = 1, learning_rate: float = 1e-5, val_percent: float = 0.1, @@ -179,7 +194,7 @@ def train_model( # 'epoch': epoch # }) pbar.set_postfix(**{'loss (batch)': loss.item()}) - + batch_losses.append(loss.item()) # Evaluation round division_step = (n_train // (5 * batch_size)) if division_step > 0: @@ -195,6 +210,7 @@ def train_model( val_score = evaluate(model, val_loader, device, amp) scheduler.step(val_score) + val_dice_scores.append(val_score) logging.info('Validation Dice score: {}'.format(val_score)) try: pass @@ -220,10 +236,36 @@ def train_model( torch.save(state_dict, str(dir_checkpoint / 'checkpoint_epoch{}.pth'.format(epoch))) logging.info(f'Checkpoint {epoch} saved!') + plt.figure(figsize=(10, 5)) + plt.plot(batch_losses, label='Batch Loss') + plt.title('Batch Loss During Training') + plt.xlabel('Batch') + plt.ylabel('Loss') + plt.legend() + plt.show() + + val_dice_scores_cpu = [score.cpu().item() for score in val_dice_scores] + # Plot validation Dice scores + plt.figure(figsize=(10, 5)) + plt.plot(val_dice_scores_cpu, label='Validation Dice Score') + plt.title('Validation Dice Score') + plt.xlabel('Epoch') + plt.ylabel('Dice Score') + plt.legend() + plt.show() + + # # Confusion Matrix Plot + # plt.figure(figsize=(8, 6)) + # sns.heatmap(conf_matrix_total, annot=True, fmt='d', cmap='Blues') + # plt.title('Confusion Matrix') + # plt.xlabel('Predicted') + # plt.ylabel('True') + # plt.show() + def get_args(): parser = argparse.ArgumentParser(description='Train the UNet on images and target masks') - parser.add_argument('--epochs', '-e', metavar='E', type=int, default=5, help='Number of epochs') + parser.add_argument('--epochs', '-e', metavar='E', type=int, default=50, help='Number of epochs') parser.add_argument('--batch-size', '-b', dest='batch_size', metavar='B', type=int, default=1, help='Batch size') parser.add_argument('--learning-rate', '-l', metavar='LR', type=float, default=1e-5, help='Learning rate', dest='lr') @@ -232,7 +274,7 @@ def get_args(): parser.add_argument('--validation', '-v', dest='val', type=float, default=10.0, help='Percent of the data that is used as validation (0-100)') parser.add_argument('--amp', action='store_true', default=False, help='Use mixed precision') - parser.add_argument('--bilinear', action='store_true', default=False, help='Use bilinear upsampling') + parser.add_argument('--bilinear', action='store_true', default=False, help='Use bilinear upksampling') parser.add_argument('--classes', '-c', type=int, default=2, help='Number of classes') return parser.parse_args() From 04b1e255b77de87f7b794cf0714b3ed8acddd3e1 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 15:44:11 +1000 Subject: [PATCH 24/28] topic-recognition - more updates --- recognition/2d_unet_s46974426/README.md | 4 +- .../images/console_running.png | Bin 0 -> 21326 bytes recognition/2d_unet_s46974426/predict.py | 131 ++++++++++++++---- 3 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 recognition/2d_unet_s46974426/images/console_running.png diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index 98b536bd6..42ac783bb 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -36,4 +36,6 @@ datasets not suffixed with 'seg' (I had it the wrong way around originally). I ran the train code just for the first 5 epochs and a graph showing the batch loss and a graph showing the dice score can both be seen in the images folder. -I then ran it for 50 epochs and the graphs similar to above are in the images folder. \ No newline at end of file +I then ran it for 50 epochs and the graphs similar to above are in the images folder. + +Proof of the console running progress can also be seen in the console_running image in the images folder. \ No newline at end of file diff --git a/recognition/2d_unet_s46974426/images/console_running.png b/recognition/2d_unet_s46974426/images/console_running.png new file mode 100644 index 0000000000000000000000000000000000000000..cfcce15b946d2eb2241aa290b913e4255e195ecb GIT binary patch literal 21326 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 literal 0 HcmV?d00001 diff --git a/recognition/2d_unet_s46974426/predict.py b/recognition/2d_unet_s46974426/predict.py index 9ed6f535d..fecfe2254 100644 --- a/recognition/2d_unet_s46974426/predict.py +++ b/recognition/2d_unet_s46974426/predict.py @@ -1,32 +1,115 @@ -import torch +import argparse +import logging +import os + import numpy as np -import matplotlib.pyplot as plt -from modules import UNet -from dataset import load_data_2D # Ensure to adjust this based on your structure +import torch +import torch.nn.functional as F +from PIL import Image +from torchvision import transforms -def predict(model, image): - model.eval() - with torch.no_grad(): - output = model(image.unsqueeze(0)) - return torch.sigmoid(output).squeeze(0) +from modules import BasicDataset, UNet, plot_img_and_mask -if __name__ == "__main__": - model = UNet(in_channels=1, out_channels=1) - model.load_state_dict(torch.load('model_checkpoint.pth')) # Load your trained model +def predict_img(net, + full_img, + device, + scale_factor=1, + out_threshold=0.5): + net.eval() + img = torch.from_numpy(BasicDataset.preprocess(None, full_img, scale_factor, is_mask=False)) + img = img.unsqueeze(0) + img = img.to(device=device, dtype=torch.float32) - # Load a sample image for prediction - sample_image_paths = ["path/to/sample/image.nii.gz"] - sample_images = load_data_2D(sample_image_paths) + with torch.no_grad(): + output = net(img).cpu() + output = F.interpolate(output, (full_img.size[1], full_img.size[0]), mode='bilinear') + if net.n_classes > 1: + mask = output.argmax(dim=1) + else: + mask = torch.sigmoid(output) > out_threshold - preds = predict(model, sample_images[0]) # Assuming sample_images is the first loaded sample + return mask[0].long().squeeze().numpy() - # Visualization - plt.subplot(1, 2, 1) - plt.title('Input Image') - plt.imshow(sample_images[0][0, :, :], cmap='gray') # Assuming single channel - plt.subplot(1, 2, 2) - plt.title('Predicted Mask') - plt.imshow(preds.numpy(), cmap='gray') +def get_args(): + parser = argparse.ArgumentParser(description='Predict masks from input images') + parser.add_argument('--model', '-m', default='MODEL.pth', metavar='FILE', + help='Specify the file in which the model is stored') + parser.add_argument('--input', '-i', metavar='INPUT', nargs='+', help='Filenames of input images', required=True) + parser.add_argument('--output', '-o', metavar='OUTPUT', nargs='+', help='Filenames of output images') + parser.add_argument('--viz', '-v', action='store_true', + help='Visualize the images as they are processed') + parser.add_argument('--no-save', '-n', action='store_true', help='Do not save the output masks') + parser.add_argument('--mask-threshold', '-t', type=float, default=0.5, + help='Minimum probability value to consider a mask pixel white') + parser.add_argument('--scale', '-s', type=float, default=0.5, + help='Scale factor for the input images') + parser.add_argument('--bilinear', action='store_true', default=False, help='Use bilinear upsampling') + parser.add_argument('--classes', '-c', type=int, default=2, help='Number of classes') - plt.show() + return parser.parse_args() + + +def get_output_filenames(args): + def _generate_name(fn): + return f'{os.path.splitext(fn)[0]}_OUT.png' + + return args.output or list(map(_generate_name, args.input)) + + +def mask_to_image(mask: np.ndarray, mask_values): + if isinstance(mask_values[0], list): + out = np.zeros((mask.shape[-2], mask.shape[-1], len(mask_values[0])), dtype=np.uint8) + elif mask_values == [0, 1]: + out = np.zeros((mask.shape[-2], mask.shape[-1]), dtype=bool) + else: + out = np.zeros((mask.shape[-2], mask.shape[-1]), dtype=np.uint8) + + if mask.ndim == 3: + mask = np.argmax(mask, axis=0) + + for i, v in enumerate(mask_values): + out[mask == i] = v + + return Image.fromarray(out) + + +if __name__ == '__main__': + args = get_args() + logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') + + in_files = args.input + out_files = get_output_filenames(args) + + net = UNet(n_channels=3, n_classes=args.classes, bilinear=args.bilinear) + + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + logging.info(f'Loading model {args.model}') + logging.info(f'Using device {device}') + + net.to(device=device) + state_dict = torch.load(args.model, map_location=device) + mask_values = state_dict.pop('mask_values', [0, 1]) + net.load_state_dict(state_dict) + + logging.info('Model loaded!') + + for i, filename in enumerate(in_files): + logging.info(f'Predicting image {filename} ...') + img = Image.open(filename) + + mask = predict_img(net=net, + full_img=img, + scale_factor=args.scale, + out_threshold=args.mask_threshold, + device=device) + + if not args.no_save: + out_filename = out_files[i] + result = mask_to_image(mask, mask_values) + result.save(out_filename) + logging.info(f'Mask saved to {out_filename}') + + if args.viz: + logging.info(f'Visualizing results for image {filename}, close to continue...') + plot_img_and_mask(img, mask) \ No newline at end of file From 0c0407201b6fe0e5642ec5c762ecd67babb36162 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 16:31:48 +1000 Subject: [PATCH 25/28] topic-recognition - spelling mistakes in report cleared up --- recognition/2d_unet_s46974426/README.md | 44 ++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index 42ac783bb..e307457bb 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -1,27 +1,31 @@ Using 2D UNet to segment the HipMRI Study on Prostate Cancer dataset +The task for this report was to create 4 files, modules.py, train.py, dataset.py and predict.py to +load and segment the HipMRI study as a 2d Unet using Pytorch and the direct task description taken +from Blackboard is below. + Task Description from Blackboard: "Segment the HipMRI Study on Prostate Cancer (see Appendix for link) -using the processed 2D slices (2D images) available here with the 2D UNet [1] with all labels having a +using the processed 2D slices (2D images) available here with the 2D UNet [1] with all labels having a minimum Dice similarity coefficient of 0.75 on the test set on the prostate label. You will need to load Nifti file format and sample code is provided in Appendix B. [Easy Difficulty]" -I quickly want to mention that I prefixed each commit with 'topic recognition' this was a force of habit, -typically when working on git repositories I branch the solution to a branch named after a change request -e.g. "CR-123" and prefix each commit with this. +I quickly want to mention that I prefixed each commit with 'topic recognition' this was a force of habit, +typically when I have worked on git repositories I first branch the solution named after a change request +e.g. "CR-123" and prefix each commit with the name of the CR. An initial test code was run to just visualise one of the slices before using 2D UNet to get a sense of -what the images look like. The resuling image afer test.py was run can be seen in slice_print_from_initial_test -in the images folder. +what the images look like. The resulting image after test.py was run can be seen in +slice_print_from_initial_test in the images folder. -The data loader was run in a simple for to check that it worked, it was ~50% successful when it errorred due -to image sizing issue. To resolve this, an image resizing function was added to be called by the data -loader. The completed data_loader test output can be seen in data_loader_test.png in the images folder. +The data loader was run in a simple for to check that it worked, it was ~50% successful when it errored due +to image sizing issue. To resolve this, an image resizing function was added to be called by the data loader. +The completed data_loader test output can be seen in data_loader_test.png in the images folder. -After messing around with fixing errors from the original versions of modules, dataset, predict and train I -eventually gave up on those versions as they would not run. +After messing around with fixing errors from the original versions I had tried of the modules, dataset, predict +and train scripts I eventually gave up as they would not run. -I went online and found a similar example of a 2d UNet implemented using pytorch and adapted the code to suit -my problem and reference to this can be seen below. +I went online and found a similar example of a 2d UNet implemented using pytorch and adapted the code to suit +my problem and reference to this repository can be seen below. Author: milesial Date: 11/02/2024 @@ -30,12 +34,14 @@ Code version: 475 Type (e.g. computer program, source code): computer program Web address or publisher (e.g. program publisher, URL): https://github.com/milesial/Pytorch-UNet -Also during this process I discovered that the masks were inb the segment datasets and the images were in the -datasets not suffixed with 'seg' (I had it the wrong way around originally). +Also, during this process, I discovered that the masks were in the segment datasets and the images were +in the datasets not suffixed with 'seg' (I had it the wrong way around originally). -I ran the train code just for the first 5 epochs and a graph showing the batch loss and a graph showing the dice -score can both be seen in the images folder. +After attempting to run the train.py file and fixing errors as they occurred, I was eventually able to +run the train.py code in full to generate some loss and dice coefficient-based validation plots. -I then ran it for 50 epochs and the graphs similar to above are in the images folder. +I ran the train code for the first 5 epochs and a graph showing the batch loss and a graph showing the +dice score can both be seen in the images folder. I then ran it for 50 epochs and the graphs similar to +above are in the images folder. The console running progress can also be seen in the console_running image +in the images folder. -Proof of the console running progress can also be seen in the console_running image in the images folder. \ No newline at end of file From 5a223fdb522f16003559a9b0129be9bff2457bf1 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 20:26:15 +1000 Subject: [PATCH 26/28] topic-recognition - updated script comments and updated the report --- recognition/2d_unet_s46974426/README.md | 21 ++++++++ recognition/2d_unet_s46974426/dataset.py | 8 +++ recognition/2d_unet_s46974426/modules.py | 64 ++++++++++++++++++++++-- recognition/2d_unet_s46974426/train.py | 41 ++++++++++++++- 4 files changed, 130 insertions(+), 4 deletions(-) diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index e307457bb..a26fe42d4 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -45,3 +45,24 @@ dice score can both be seen in the images folder. I then ran it for 50 epochs an above are in the images folder. The console running progress can also be seen in the console_running image in the images folder. +This final part will outline a description of working principles of the algorithm and the problem it solves. +The Pytorch UNet is comprised of four parts, an encoder, decoder, bottleneck and a convolutional layer. +The modules script contains the UNet’s definition. It also includes the dice coefficient handling to calculate +dice loss which measures the overlap of two images in order to quantify a segmentation model’s accuracy. +I also added a function to combine two datasets (the segment images and masks), this is because datasets +what include both segments and masks are typically used in UNet algorithms. The modules script also included +some basic dataset classes, a method to load images, check uniqueness of masks and some basic plotting logic. + +The train script initialises and loads the UNet model defined in the modules and then trains it on the +segmentation dataset. Before this is done however, it is transformed and loaded as 2d data using the provided +load_data_2d function in the task appendix. The train script handles defining the main train loop, iterating +over the data in batches, calculating losses and dice scores, which are then plotted after the algorithm has +completed. It also handles saving progress while the training loop completes each epoch, which is made up of +a number of batches (typically 5-6 in this case). + +The dataset script just contains the load_data_2d method as seen in the appendix of the task sheet. It also +contains a data transformation function to make the image dimensions consistent. + +Finally, the predict script’s purpose is to generate mask predictions of new images on a trained and saved UNet model. + + diff --git a/recognition/2d_unet_s46974426/dataset.py b/recognition/2d_unet_s46974426/dataset.py index c0346647b..7e80b7ae6 100644 --- a/recognition/2d_unet_s46974426/dataset.py +++ b/recognition/2d_unet_s46974426/dataset.py @@ -6,6 +6,14 @@ import torch from torch.utils.data import Dataset # Add this import for Dataset + +''' + Resizes the images to all be consistent (this is required for the data loading process in load_data_2d) + + Parameters: + - image: image that is being resized + - target_shape: shape the image is being resized too e.g. 256*128 pixels +''' def resize_image(image, target_shape): """Resize image to the target shape using OpenCV.""" return cv2.resize(image, (target_shape[1], target_shape[0]), interpolation=cv2.INTER_LINEAR) diff --git a/recognition/2d_unet_s46974426/modules.py b/recognition/2d_unet_s46974426/modules.py index aba86d710..16a3638ac 100644 --- a/recognition/2d_unet_s46974426/modules.py +++ b/recognition/2d_unet_s46974426/modules.py @@ -80,7 +80,9 @@ def forward(self, x1, x2): x = torch.cat([x2, x1], dim=1) return self.conv(x) - +''' + Definition of a OutConv class +''' class OutConv(nn.Module): def __init__(self, in_channels, out_channels): super(OutConv, self).__init__() @@ -89,7 +91,9 @@ def __init__(self, in_channels, out_channels): def forward(self, x): return self.conv(x) - +''' + Definition of a Pytorch UNet to be trained on the loaded 2d segmentation dataset +''' class UNet(nn.Module): def __init__(self, n_channels, n_classes, bilinear=False): super(UNet, self).__init__() @@ -134,6 +138,15 @@ def use_checkpointing(self): self.up4 = torch.utils.checkpoint(self.up4) self.outc = torch.utils.checkpoint(self.outc) +''' + Handles calculating dice coefficent between a predicted mask and a true mask + + Parameters: + -input: predicted mask (as a tensor) + -target: true mask (as a tensor) + -reduce_batch_first: wheater to average over batch dimension + -epsilon: small value to prevent division by 0 +''' def dice_coeff(input: Tensor, target: Tensor, reduce_batch_first: bool = False, epsilon: float = 1e-6): # Average of Dice coefficient for all batches, or for a single mask assert input.size() == target.size() @@ -148,17 +161,38 @@ def dice_coeff(input: Tensor, target: Tensor, reduce_batch_first: bool = False, dice = (inter + epsilon) / (sets_sum + epsilon) return dice.mean() +''' + Calculates dice coefficient across multiple classes + Parameters: + -input: predicted mask (as a tensor) + -target: true mask (as a tensor) + -reduce_batch_first: wheater to average over batch dimension + -epsilon: small value to prevent division by 0 +''' def multiclass_dice_coeff(input: Tensor, target: Tensor, reduce_batch_first: bool = False, epsilon: float = 1e-6): # Average of Dice coefficient for all classes return dice_coeff(input.flatten(0, 1), target.flatten(0, 1), reduce_batch_first, epsilon) +''' + Calculates dice loss for an image segmentation + Parameters: + -input: predicted mask (as a tensor) + -target: true mask (as a tensor) + -multiclass: is multiclass? +''' def dice_loss(input: Tensor, target: Tensor, multiclass: bool = False): # Dice loss (objective to minimize) between 0 and 1 fn = multiclass_dice_coeff if multiclass else dice_coeff return 1 - fn(input, target, reduce_batch_first=True) +''' + Combines two datasets, in this case used to combine the mask and segmetation datasets + + Initialisation: + -takes two datasets to intialise the class and returns them in a tuple +''' class CombinedDataset(Dataset): def __init__(self, images, image_masks, transform=None): """ @@ -198,6 +232,12 @@ def __getitem__(self, idx): return image, mask +''' + Loads a specified image + + Parameters: + -filename: path to the image being loaded +''' def load_image(filename): ext = splitext(filename)[1] if ext == '.npy': @@ -207,7 +247,14 @@ def load_image(filename): else: return Image.open(filename) +''' + Identifies unique values in a mask + Parameters: + -idx: string identifier of a mask + -mask_dir: directory contianing the masks + -mask_suffix: identifies the mask by combining this and the idx +''' def unique_mask_values(idx, mask_dir, mask_suffix): mask_file = list(mask_dir.glob(idx + mask_suffix + '.*'))[0] mask = np.asarray(load_image(mask_file)) @@ -219,7 +266,9 @@ def unique_mask_values(idx, mask_dir, mask_suffix): else: raise ValueError(f'Loaded masks should have 2 or 3 dimensions, found {mask.ndim}') - +''' + Defines a BasicDatabase class +''' class BasicDataset(Dataset): def __init__(self, images_dir: str, mask_dir: str, scale: float = 1.0, mask_suffix: str = ''): self.images_dir = Path(images_dir) @@ -297,11 +346,20 @@ def __getitem__(self, idx): } +''' + Defines a CarvanaDataset class +''' class CarvanaDataset(BasicDataset): def __init__(self, images_dir, mask_dir, scale=1): super().__init__(images_dir, mask_dir, scale, mask_suffix='_mask') +''' + Funtion to plot image and its corrolated mask + Paramters: + -img: image + -mask: mask +''' def plot_img_and_mask(img, mask): classes = mask.max() + 1 fig, ax = plt.subplots(1, classes + 1) diff --git a/recognition/2d_unet_s46974426/train.py b/recognition/2d_unet_s46974426/train.py index 431137dd0..e06782def 100644 --- a/recognition/2d_unet_s46974426/train.py +++ b/recognition/2d_unet_s46974426/train.py @@ -21,6 +21,7 @@ import wandb +#setting dataset paths and empty arrays for plotting dir_img = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_test') dir_mask = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_seg_test') dir_img_val = Path('C:/Users/rober/Desktop/COMP3710/keras_slices_validate') @@ -31,6 +32,15 @@ val_dice_scores = [] conf_matrix_total = None +''' + Computes the dice score of the UNet, a measure of how well the model segments images by comparing the predicted and true masks + + Parameters: + -net: the UNet + -dataloader: dataloader that provides validation data in batches + -device: the device where the model is run on (in this case cuda) + -amp: depricated from reference code +''' def evaluate(net, dataloader, device, amp): net.eval() num_val_batches = len(dataloader) @@ -74,6 +84,22 @@ def evaluate(net, dataloader, device, amp): return dice_score / max(num_val_batches, 1) +''' + Main training model for training the segmentation model (UNet) + + Parameters: + -model: the UNet model + -device: device the model is run on (cuda) + -epochs: the number of epochs run + -batch_size: the size of the batch + -learning_rate: the learning rate of the model + -save_checkpoint: boolean to determine if the model is saves based on progress + -img_scale: scaling factor for images (default seems to be typically 0,5) + -amp: boolean to determine use of automatic mixed precision (amp) + -weight_decay: regularisation parameter to prevent overfitting + -momentum: hyperparam for optimiser to accelerate SGD + -gradient_clipping: clips back progogation to prevent exploding gradients +''' def train_model( model, device, @@ -262,7 +288,20 @@ def train_model( # plt.ylabel('True') # plt.show() - +''' + Command-line interface to train the UNet model on images and corresponding target masks + + Parameters: + --epochs: number of epochs run + --batch-size: size of each batch + --learning-rate: learning rate of model + --load: specifies path to pre-trained model + --scale: downscaling factor + --validation: dataset percentage used for validation + --amp: whether to use automatic mixed precision (amp) + --bilinear: whether to use bilinear upsampling + --classes: number of classes for the segmentation task +''' def get_args(): parser = argparse.ArgumentParser(description='Train the UNet on images and target masks') parser.add_argument('--epochs', '-e', metavar='E', type=int, default=50, help='Number of epochs') From ea1d8509f7d46cf77136e099cb0ecc9ad8a58559 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 20:28:50 +1000 Subject: [PATCH 27/28] topic-recognition - comment updated --- recognition/2d_unet_s46974426/predict.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/recognition/2d_unet_s46974426/predict.py b/recognition/2d_unet_s46974426/predict.py index fecfe2254..8255e6679 100644 --- a/recognition/2d_unet_s46974426/predict.py +++ b/recognition/2d_unet_s46974426/predict.py @@ -10,6 +10,9 @@ from modules import BasicDataset, UNet, plot_img_and_mask +''' + This script is unaltered from the example UNet usage (referenced in the report) as I was not super sure what to do with it and ran out of time to implement for this segmentation example. +''' def predict_img(net, full_img, device, From c7f54eb1aea36e26d43b49012ed6589ad1d41f82 Mon Sep 17 00:00:00 2001 From: Robert Slomka Date: Fri, 25 Oct 2024 20:34:41 +1000 Subject: [PATCH 28/28] topic-recognition - could not update pdf submission note in report --- recognition/2d_unet_s46974426/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/recognition/2d_unet_s46974426/README.md b/recognition/2d_unet_s46974426/README.md index a26fe42d4..782bff597 100644 --- a/recognition/2d_unet_s46974426/README.md +++ b/recognition/2d_unet_s46974426/README.md @@ -1,3 +1,7 @@ +PLEASE NOTE: the version is different from pdf submission because could not re-submit? + +COMP3710 2D UNet Report + Using 2D UNet to segment the HipMRI Study on Prostate Cancer dataset The task for this report was to create 4 files, modules.py, train.py, dataset.py and predict.py to