How to return the 3D volume from dataset class?

aleemsidra: <p>I am new to Pytorch and I am working with 3D dataset. In the custom class I want to read a 3D image and return it as one 3D image  and not as 2D slices. Now in dataloader I want to  make sure that if batch size is set to two, then two 3D images are being passed. How can I do that with custom dataset class.</p>
<p>I am able to read the 3D images using <code>nibabel</code>. But I am not sure that, how to enusre that one 3D image should be considered as one entity.</p>

ptrblck: <p>Loading the volumes and transforming them into a tensor should work as the <code>DataLoader</code> shouldn’t care about the dimensions of your input as long as it can <code>stack</code> the samples in the batch dimension:</p>
<pre><code class="lang-python">class MyDataset(Dataset):
    def __init__(self):
        self.data = torch.randn(100, 3, 128, 128, 128) # [nb_samples, channels, depth, width, height]
        
    def __getitem__(self, index):
        x = self.data[index]
        return x
    
    def __len__(self):
        return len(self.data)

dataset = MyDataset()
loader = DataLoader(dataset, batch_size=2)

x = next(iter(loader))
print(x.shape)
# torch.Size([2, 3, 128, 128, 128]) # [batch_size, channels, depth, height, width]
</code></pre>

aleemsidra: <p><a class="mention" href="/u/ptrblck">@ptrblck</a> , below is the part of my dataset class code:</p>
<pre><code class="lang-auto">for i, f in enumerate(files):
            print("file_name", f)
            nib_file = nib.load(os.path.join(images_path, f))
            img = nib_file.get_fdata('unchanged', dtype=np.float32) 
            lbl = nib.load(os.path.join(data_path, 'Silver-standard', self.folder, f[:-7] + '_ss.nii.gz')).get_fdata(
                'unchanged', dtype=np.float32)

            if self.scale:
                transformed = scaler.fit_transform(np.reshape(img, (-1, 1)))
                self.img = np.reshape(transformed, img.shape)         
            if not self.sagittal:
                self.img = np.moveaxis(img, -1, 0)
          
            if self.rotate:
                self.img = np.rot90(img, axes=(1, 2))

            if img.shape[1] != img.shape[2]:
                self.img = self.pad_image(img)
 
            if not self.sagittal:
                self.lbl = np.moveaxis(lbl, -1, 0)
            if self.rotate:
                self.lbl = np.rot90(lbl, axes=(1, 2))
            if lbl.shape[1] != lbl.shape[2]:
                self.lbl = self.pad_image(lbl)
         
            spacing = [nib_file.header.get_zooms()] * img.shape[0]
            self.voxel_dim = spacing  
        
            if i ==1: # done only to understand the shapes
                break


def __getitem__(self, idx):

        data = torch.from_numpy(self.img[idx])
        labels = torch.from_numpy(self.lbl[idx])
      
 
        return data, labels
</code></pre>
<p>Now in order to check the data is loaded as desired, I am trying to check the dimesion of 3D image on the first index of<code> train_data</code> as follows:</p>
<pre><code class="lang-auto">train_data = cc359_volume(config)
 print("shape", train_data[0][1])
</code></pre>
<p>I expected it to give <code>200, 256,256</code>, as nib_file_dim was (200, 256, 256) but it give the shape:</p>
<pre><code class="lang-auto">shape torch.Size([256, 256])
</code></pre>
<p>That means, the dataset class is not returning 3D volume as one entity. Rather, it is giving back one slice as indifvidual entity. Also I checked the dimesnion in loader as follows:</p>
<pre><code class="lang-auto">train_features, train_labels = next(iter(train_loader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
</code></pre>
<p>It gives the following:</p>
<p>Feature batch shape: torch.Size([2, 256, 256])<br>
Labels batch shape: torch.Size([2, 256, 256])</p>
<p>How can I get the datset to return the image as whole.</p>

ptrblck: <p>Negative strides are not supported and e.g. the <code>np.rot90</code> transformation would create such negative strides as seen here:</p>
<pre><code class="lang-python">img = np.random.randn(3, 224, 224, 224)

img = np.moveaxis(img, -1, 0)
print(img.strides)
# (8, 89915392, 401408, 1792)

# works
x = torch.from_numpy(img)

img = np.rot90(img, axes=(1, 2))
print(img.strides)
# (8, -401408, 89915392, 1792)

# breaks
x = torch.from_numpy(img)
# ValueError: At least one stride in the given numpy array is negative, and tensors with negative strides are not currently supported. (You can probably work around this by making a copy of your array  with array.copy().) 

# works again
x = torch.from_numpy(img.copy())
</code></pre>
<p>As the error message suggests, <code>copy()</code> the numpy array before trying to transform it into a PyTorch tensor.</p>

aleemsidra: <aside class="quote no-group quote-modified" data-username="aleemsidra" data-post="3" data-topic="175718">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/aleemsidra/48/38723_2.png" class="avatar"> aleemsidra:</div>
<blockquote>
<pre><code class="lang-auto">for i, f in enumerate(files):
            print("file_name", f)
            nib_file = nib.load(os.path.join(images_path, f))
            img = nib_file.get_fdata('unchanged', dtype=np.float32) 
            lbl = nib.load(os.path.join(data_path, 'Silver-standard', self.folder, f[:-7] + '_ss.nii.gz')).get_fdata(
                'unchanged', dtype=np.float32)

            if self.scale:
                transformed = scaler.fit_transform(np.reshape(img, (-1, 1)))
                self.img = np.reshape(transformed, img.shape)         
            if not self.sagittal:
                self.img = np.moveaxis(img, -1, 0)
          
            if self.rotate:
                self.img = np.rot90(img, axes=(1, 2))

            if img.shape[1] != img.shape[2]:
                self.img = self.pad_image(img)
 
            if not self.sagittal:
                self.lbl = np.moveaxis(lbl, -1, 0)
            if self.rotate:
                self.lbl = np.rot90(lbl, axes=(1, 2))
            if lbl.shape[1] != lbl.shape[2]:
                self.lbl = self.pad_image(lbl)
         
            spacing = [nib_file.header.get_zooms()] * img.shape[0]
            self.voxel_dim = spacing  
        
            if i ==1: # done only to understand the shapes
                break

def __len__(self):
        return len(self.data_apth)

def __getitem__(self, idx):

        data = torch.from_numpy(self.img[idx])
        labels = torch.from_numpy(self.lbl[idx])
      
 
        return data, labels
</code></pre>
</blockquote>
</aside>
<p>In <code>_getitem__(self, idx)</code>, <code>data = torch.from_numpy(self.img[idx])</code> is retutning <code>256,256</code> (one slice dimesion). Why it not returning <code>200, 256, 256</code>.</p>

ptrblck: <p>I don’t know and oyu would need to check the shape of <code>img</code> after loading it via <code>nib_file.get_fdata</code> and make sure no transformation is changing its shape in an unwanted way.</p>

aleemsidra: <p><a class="mention" href="/u/ptrblck">@ptrblck</a></p>
<p>Transformations were not changing the shape. Previously in get_item()_, I was indexing on image it self like this:</p>
<pre><code class="lang-auto">
def __getitem__(self, idx):
        data = self.data[idx]  #self.data has one image
</code></pre>
<p>That was changing the dimension and was taking the slices and givng <code>[256,256]</code>. I am now indexing on the file list in the folder itself as follows:</p>
<pre><code class="lang-auto">def __len__(self):
        return len(self.files)

    def __getitem__(self, idx):

        nib_file = nib.load(os.path.join(self.images_path, self.files[idx]))
</code></pre>
<p>which gives the dimension [200,256,256]. Is my understanding correct?</p>

ptrblck: <p>Yes, loading the entire volume in the <code>__getitem__</code> method is the right approach and I assumed <code>self.data</code> contained all volumes.</p>