Why is RandomCrop with size 84 and padding 8 returning an image size of 84 and not 100 in pytorch?

username: Brando_Miranda
cooked: <p>I was using the mini-imagenet data set and noticed this line of code:</p>
<pre><code class="lang-auto">    elif data_augmentation == 'lee2019:
        normalize = Normalize(
            mean=[120.39586422 / 255.0, 115.59361427 / 255.0, 104.54012653 / 255.0],
            std=[70.68188272 / 255.0, 68.27635443 / 255.0, 72.54505529 / 255.0],
        )
        train_data_transforms = Compose([
            ToPILImage(),
            RandomCrop(84, padding=8),
            ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4),
            RandomHorizontalFlip(),
            ToTensor(),
            normalize,
        ])
        test_data_transforms = Compose([
            normalize,
        ])
</code></pre>
<p>but when I checked the image size it was 84 instead of 100 (after adding padding):</p>
<pre><code class="lang-auto">X.size()=torch.Size([50, 3, 84, 84])
</code></pre>
<p>what is going on with this? Shouldn’t it be 100?</p>
<hr>
<p>reproduction:</p>
<pre><code class="lang-auto">import random
from typing import Callable

import learn2learn as l2l
import numpy as np
import torch
from learn2learn.data import TaskDataset, MetaDataset, DataDescription
from learn2learn.data.transforms import TaskTransform
from torch.utils.data import Dataset


class IndexableDataSet(Dataset):

    def __init__(self, datasets):
        self.datasets = datasets

    def __len__(self) -&gt; int:
        return len(self.datasets)

    def __getitem__(self, idx: int):
        return self.datasets[idx]


class SingleDatasetPerTaskTransform(Callable):
    """
    Transform that samples a data set first, then creates a task (e.g. n-way, k-shot) and finally
    applies the remaining task transforms.
    """

    def __init__(self, indexable_dataset: IndexableDataSet, cons_remaining_task_transforms: Callable):
        """

        :param: cons_remaining_task_transforms; constructor that builds the remaining task transforms. Cannot be a list
        of transforms because we don't know apriori which is the data set we will use. So this function should be of
        type MetaDataset -&gt; list[TaskTransforms] i.e. given the dataset it returns the transforms for it.
        """
        self.indexable_dataset = MetaDataset(indexable_dataset)
        self.cons_remaining_task_transforms = cons_remaining_task_transforms

    def __call__(self, task_description: list):
        """
        idea:
        - receives the index of the dataset to use
        - then use the normal NWays l2l function
        """
        # - this is what I wish could have gone in a seperate callable transform, but idk how since the transforms take apriori (not dynamically) which data set to use.
        i = random.randint(0, len(self.indexable_dataset) - 1)
        task_description = [DataDescription(index=i)]  # using this to follow the l2l convention

        # - get the sampled data set
        dataset_index = task_description[0].index
        dataset = self.indexable_dataset[dataset_index]
        dataset = MetaDataset(dataset)

        # - use the sampled data set to create task
        remaining_task_transforms: list[TaskTransform] = self.cons_remaining_task_transforms(dataset)
        description = None
        for transform in remaining_task_transforms:
            description = transform(description)
        return description


def sample_dataset(dataset):
    def sample_random_dataset(x):
        print(f'{x=}')
        i = random.randint(0, len(dataset) - 1)
        return [DataDescription(index=i)]
        # return dataset[i]

    return sample_random_dataset


def get_task_transforms(dataset: IndexableDataSet) -&gt; list[TaskTransform]:
    """
    :param dataset:
    :return:
    """
    transforms = [
        sample_dataset(dataset),
        l2l.data.transforms.NWays(dataset, n=5),
        l2l.data.transforms.KShots(dataset, k=5),
        l2l.data.transforms.LoadData(dataset),
        l2l.data.transforms.RemapLabels(dataset),
        l2l.data.transforms.ConsecutiveLabels(dataset),
    ]
    return transforms


def print_datasets(dataset_lst: list):
    for dataset in dataset_lst:
        print(f'\n{dataset=}\n')


def get_indexable_list_of_datasets_mi_and_cifarfs(root: str = '~/data/l2l_data/') -&gt; IndexableDataSet:
    from learn2learn.vision.benchmarks import mini_imagenet_tasksets
    datasets, transforms = mini_imagenet_tasksets(root=root)
    mi = datasets[0].dataset

    from learn2learn.vision.benchmarks import cifarfs_tasksets
    datasets, transforms = cifarfs_tasksets(root=root)
    cifarfs = datasets[0].dataset

    dataset_list = [mi, cifarfs]

    dataset_list = [l2l.data.MetaDataset(dataset) for dataset in dataset_list]
    dataset = IndexableDataSet(dataset_list)
    return dataset


# -- tests

def loop_through_l2l_indexable_datasets_test():
    """
    """
    # - for determinism
    random.seed(0)
    torch.manual_seed(0)
    np.random.seed(0)

    # - options for number of tasks/meta-batch size
    batch_size: int = 10

    # - create indexable data set
    indexable_dataset: IndexableDataSet = get_indexable_list_of_datasets_mi_and_cifarfs()

    # - get task transforms
    def get_remaining_transforms(dataset: MetaDataset) -&gt; list[TaskTransform]:
        remaining_task_transforms = [
            l2l.data.transforms.NWays(dataset, n=5),
            l2l.data.transforms.KShots(dataset, k=5),
            l2l.data.transforms.LoadData(dataset),
            l2l.data.transforms.RemapLabels(dataset),
            l2l.data.transforms.ConsecutiveLabels(dataset),
        ]
        return remaining_task_transforms
    task_transforms: TaskTransform = SingleDatasetPerTaskTransform(indexable_dataset, get_remaining_transforms)

    # -
    taskset: TaskDataset = TaskDataset(dataset=indexable_dataset, task_transforms=task_transforms)

    # - loop through tasks
    for task_num in range(batch_size):
        print(f'{task_num=}')
        X, y = taskset.sample()
        print(f'{X.size()=}')
        print(f'{y.size()=}')
        print(f'{y=}')
        print()

    print('-- end of test --')


# -- Run experiment

if __name__ == "__main__":
    import time
    from uutils import report_times

    start = time.time()
    # - run experiment
    loop_through_l2l_indexable_datasets_test()
    # - Done
    print(f"\nSuccess Done!: {report_times(start)}\a")
</code></pre>
<p>context: <a href="https://github.com/learnables/learn2learn/issues/333" class="inline-onebox" rel="noopener nofollow ugc">Creating a task loader for list of data sets (similar to meta-data set) · Issue #333 · learnables/learn2learn · GitHub</a></p>
<p>cross: <a href="https://stackoverflow.com/questions/72208865/why-is-randomcrop-with-size-84-and-padding-8-returning-an-image-size-of-84-and-n" class="inline-onebox" rel="noopener nofollow ugc">deep learning - Why is RandomCrop with size 84 and padding 8 returning an image size of 84 and not 100 in pytorch? - Stack Overflow</a></p>
<aside class="onebox allowlistedgeneric" data-onebox-src="https://www.reddit.com/r/pytorch/comments/uno1ih/why_is_randomcrop_with_size_84_and_padding_8/">
  <header class="source">
      <img src="https://www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png" class="site-icon" width="192" height="192">

      <a href="https://www.reddit.com/r/pytorch/comments/uno1ih/why_is_randomcrop_with_size_84_and_padding_8/" target="_blank" rel="noopener nofollow ugc">reddit</a>
  </header>

  <article class="onebox-body">
    <img src="https://external-preview.redd.it/QgPvRTknlY3rMNDqH1k4I37XGiq9tZF_FsygC_Xht4o.jpg?auto=webp&amp;s=8cd5e918e2bde6ca72d4445d6fc007f203689799" class="thumbnail onebox-avatar" width="316" height="316">

<h3><a href="https://www.reddit.com/r/pytorch/comments/uno1ih/why_is_randomcrop_with_size_84_and_padding_8/" target="_blank" rel="noopener nofollow ugc">r/pytorch - Why is RandomCrop with size 84 and padding 8 returning an image...</a></h3>

  <p>0 votes and 0 comments so far on Reddit</p>


  </article>

  <div class="onebox-metadata">
    
    
  </div>

  <div style="clear: both"></div>
</aside>


username: Andrei_Cristea
cooked: <p>The <a href="https://pytorch.org/vision/main/generated/torchvision.transforms.RandomCrop.html" rel="noopener nofollow ugc">documentation</a> seems to say that <em>cropping is done after padding</em>. I interpret this as, it pads first, then it crops a size 84 section (which could include some of the pad).</p>

username: Brando_Miranda
cooked: <p><a class="mention" href="/u/andrei_cristea">@Andrei_Cristea</a>  where in the docs does it say the padding is applied first?</p>

username: Brando_Miranda
cooked: <p>that is really annoying, why is that comment not in the padding section <img src="https://discuss.pytorch.org/images/emoji/apple/confused.png?v=12" title=":confused:" class="emoji" alt=":confused:" loading="lazy" width="20" height="20"></p>

username: Brando_Miranda
cooked: <p>I don’t understand, why is padding done at all for an image that already fits in the smaller window? (from 224 → 84)</p>

username: Andrei_Cristea
cooked: <p>I think the idea is that someone may want the top left corner of the image to be in the center of some of the random crops, and without padding the image first this isn’t possible.</p>
<p>For your use case I recommend cropping first (with no padding) and then simply padding the result. Like this:</p>
<pre><code class="lang-auto">train_data_transforms = Compose([
    ToPILImage(),
    RandomCrop(84),  # &lt;-- remove padding here
    Pad(8),  # &lt;-- add this
    ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4),
    RandomHorizontalFlip(),
    ToTensor(),
    normalize,
])
</code></pre>