PyTorch TorchVision Deep Learning
OVERVIEW

Introduction

Think about how a photo gets ready before it's printed or posted online:

Before an AI model can look at a photo and say "that's a flower," it goes through a similar prep process. The photo gets:

To teach a model to learn to see flowers, showing the same flowers can lead to:

So to make the model learn to see flowers, each photo also gets randomly flipped, rotated, or recolored a little differently every time it's shown. This trick is called augmentation. It forces the model to learn the idea of "flower", and focus on the underlying features that make up a flower

This whole sequence of steps is an image processing pipeline. Nearly every large image AI you've used like ChatGPT, Nano Banana, Midjourney is built on a pipeline just like this one. This is made possible by one very cool library called TorchVision.

What You Will Learn

In this article, you will learn about the TorchVision library and the set of tools that it offers that make up the image processing pipeline. It offers:

Let's get to it!

TOOLKIT

What Is TorchVision?

Deep learning for computer vision comes with a lot of hidden work — loading images, augmenting them, normalizing them, batching them, defining models, and visually debugging all of it. Writing that from scratch is slow, error-prone, and a pain to maintain.

TorchVision packages all of it into one library instead. Using it gives you:

That lets you spend your time on your own computer vision problem, instead of reinventing tools that already exist. TorchVision provides those tools in four key areas:

Transforms

Reshape, rescale, and randomize images (Resize, CenterCrop, ToTensor, Normalize, and the Random* family).

Utility Functions

The visual bookkeeping: make_grid, save_image, draw_bounding_boxes, draw_segmentation_masks.

Datasets

Ready-made sets like CIFAR-10 and EMNIST, your own folders via ImageFolder, and FakeData for debugging.

Models

Pretrained networks for classification, detection, and segmentation.

The sections below work through the transforms, the utilities, and the datasets:

Setting up the Environment

We'll take this flower image as an example to walk through the transformations. Upload your own image to see it live with your own image.

Importing Torchvision & Setting Seed
from PIL import Image  # For Image I/O
import torch
from torchvision import transforms

torch.manual_seed(42)                   # Helps with reproducibility
img = Image.open("flower-1.jpg")        # PIL image · H × W × C
H, W = img.height, img.width            # PIL gives channels-last size
C = len(img.getbands())                 # e.g. 3 for RGB
print(f"Image Size: {H} x {W} x {C}")
Name: flower-1.jpg, Image Size: 512 x 512 x 3 (H x W x C), Data Type: uint8
TRANSFORMS

Image As A Tensor

TorchVision's transforms module is built around five core functions — ToTensor(), ToPILImage(), Resize(), CenterCrop(), and Normalize(). The first two helps you convert an image to a tensor and back

Hover the image below to see a pixel's raw values before and after that conversion.

transform.py — image → tensor → back
# ToTensor(): PIL → tensor · uint8 [0,255] HWC → float32 [0,1] CHW
tensor = transforms.ToTensor()(out)

# ToPILImage(): the exact inverse — tensor → PIL, back to the original image
back = transforms.ToPILImage()(tensor)

Shape

shape [3, 512, 512] · dtype uint8

Value View

hover the image →

toTensor() View

hover the image →
TRANSFORMS

Basic Preprocessing Steps

Preprocessing involes resizing, cropping and normalizing images before they are fed into a model. TorchVision provides three core transforms for this purpose:

transform.py — what changes
transform = transforms.Resize(224)
before
after
bilinear here is implemented by hand (not the browser's), matching PIL's sampling

What's interpolation mode? When you resize an image, most of the new pixels don't exist yet — the computer has to guess their color from the pixels around them. That guessing rule is the interpolation mode. nearest just copies the closest original pixel: fast, but blocky. bilinear blends the 4 nearest original pixels together: slower, but smooth. Flip the toggle above and watch the difference.

transform.py — what changes
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
])
before crop (after Resize)
after CenterCrop

Use it when validation and inference need deterministic framing — the classic pair is Resize(256) then CenterCrop(224), keeping the central portion at a fixed size.

Limitations: the crop discards edges unconditionally — usually background, sometimes your subject. Shrink it until you start losing things you care about; that is your boundary. It has no randomness, so it does not augment: for training variety, reach for RandomResizedCrop in station 03.

transform.py — what changes
transform = transforms.Compose([
    transforms.ToTensor(),                 # Normalize needs a tensor first
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225]),
])
before · input (0–1)
after · normalized · rescaled for display ⚠
before — pixel intensity per channel (0–1)
after — z-scores per channel (≈ −3 to 3, centred on 0)

Use it when feeding a pretrained backbone — use the ImageNet statistics it was trained with — or training from scratch, where you compute mean and std from your own data. Normalize is a per-channel z-score: subtract the mean, divide by the std. The histograms are the honest view — same shapes, recentred at 0 with σ≈1.

Limitations: the output is no longer an image. It has negative values, so it cannot be displayed or saved without undoing the operation — the preview above is min-max rescaled and labelled as such. Wrong statistics do not raise an error; they silently shift every input off the distribution the backbone learned. That failure mode is invisible until accuracy tells you.

TRANSFORMS

Randomizing Images For Training

A small dataset is the usual reason to augment: each random variation is effectively a new training sample, teaching the model that the label survives the change. The Random* transforms roll fresh dice on every call — here the dice are a seed you control, the same trick as torch.manual_seed(). Same seed, same "random" result.

transform.py — what changes
torch.manual_seed(7)
transform = transforms.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.4)
6 samples from the current jitter ranges (factors printed on each tile)

Note: Color can sometimes be label information for some tasks — species identified by hue, ripeness, medical staining — and jitter can erase exactly what the model needs to learn. Visualizing it like a grid can help you understand the extremity of such transformations before training your model

transform.py — what changes
torch.manual_seed(7)
transform = transforms.RandomHorizontalFlip(p=0.5)
6 draws · amber corner = this draw flipped

Use it when orientation does not define the object — true for most natural photos. Default p=0.5: half of all samples get mirrored.

Limitations: when orientation is semantic — digits, text, road signs — a flip manufactures a wrongly-labelled sample. And p is a per-sample probability, not a guarantee: at p=0.5, a run of six unflipped draws is unlikely but legal, as the seed control will show you.

transform.py — what changes
torch.manual_seed(7)
transform = transforms.RandomResizedCrop(224, scale=(0.30, 1.0))
5 draws · outline on the original shows each sampled region

Use it when you want scale and translation invariance — this is the standard ImageNet training crop. It samples a random area (between scale min and 100%, at a random aspect ratio 3/4–4/3) and resizes it to the output size, using TorchVision's exact 10-attempt sampling logic.

Limitations: at a low scale minimum, crops can miss the subject entirely — push it to 0.08 and watch subject-free tiles appear. Each one is a mislabelled training sample. The outlines on the original show you precisely which regions the sampler chose; that is the boundary you are here to find.

TRANSFORMS

Writing Your Own Transform

If you want to implement your custom tranforms, TorchVision allows it. It only requires the function you write is callable. The classic example: salt-and-pepper noise, random white and black pixels simulating a grainy sensor.

custom_transform.py — __init__ + __call__, then swap it in
class SaltAndPepperNoise:
    def __init__(self, ratio=0.5, amount=0.05):   # parameters live here
        self.ratio, self.amount = ratio, amount
    def __call__(self, img):                      # the operation lives here
        arr = np.array(img)
        n = int(arr.shape[0] * arr.shape[1] * self.amount)
        ys, xs = np.random.randint(0, arr.shape[0], n), np.random.randint(0, arr.shape[1], n)
        arr[ys, xs] = np.where(np.random.rand(n, 1) < self.ratio, 255, 0)
        return Image.fromarray(arr)

transform = SaltAndPepperNoise(ratio=0.5, amount=0.050)
TRANSFORMS

Chaining Multiple Transforms

Compose() combines multiple transforms into a single preprocessing pipeline defined as a list of operations applied in sequence. Any image passed through will come out the correct size, format, and scale. Because everything is defined in one place, it's easy to reuse and maintain consistency across datasets.

When you pass a pipeline to a TorchVision dataset, the transformation is automatically applied to every image entering a batch.

Process — a typical preprocessing pipeline:

  1. Resize() the image.
  2. CenterCrop().
  3. ToTensor().
  4. Normalize() using the ImageNet mean and standard deviations.

Pipelines built with Compose() help solve a common challenge: a small dataset. Even with a good architecture, limited data may be too little to train effectively. One solution is to create variations of each image — color jitter, rotation, noise, random crops, random resizing. This is image augmentation: each new version effectively becomes a new sample, increasing data size and variety without collecting new data.

Common random transformations include:

After applying random transforms, convert images to tensors and then normalize.

The cards below are those stages in order — follow the arrows. Drag a card to reorder it, press × to remove it, and add more from the tray. If you put, Normalize() before ToTensor() and you would get an error.

pipeline.py — rewrites itself as you edit the rail
state: PIL · uint8 · HWC

Using make_grid to Visualize multiple transformations at once and save_image() exports tensors back into image file formats like JPEG or PNG.

contact_sheet.py — what changes
from torchvision.utils import make_grid, save_image

batch = torch.stack([transform(img) for _ in range(6)])
grid  = make_grid(batch, nrow=3, padding=4, normalize=True)
save_image(grid, "contact_sheet.png")
UTILS

Drawing Boxes On An Image

Bounding boxes are a key output of object detection models, indicating the locations and categories of objects. Visualizing them helps you understand and validate predictions

Process — drawing boxes manually:

  1. Import the utilities module (contains draw_bounding_boxes).
  2. Create bounding boxes with coordinates x1, y1, x2, y2, defined as a tensor (hard-coded here).
  3. Define corresponding labels.
  4. Call draw_bounding_boxes with the image tensor, box coordinates, and labels → a new tensor containing the annotated image.
draw_boxes.py — updates as you drag

Note: coordinates here are absolute pixels to demonstrate that torchvision allows you to create simple boxes. You can play with it by dragging the corner dots to resize the boxes and edit the labels

UTILS

Using A Model To Draw Bounding Boxes

The boxes in the previous station were hand-placed. In practice the coordinates come from a detection model, and draw_bounding_boxes paints them exactly the same way.

Process — drawing boxes from a pretrained model:

  1. Use Faster R-CNN with a ResNet50-FPN backbone — Faster R-CNN for region proposal and classification, ResNet50 for feature extraction, and a feature pyramid network to detect objects at multiple scales. It's pre-trained on COCO (detects a wide range of common objects).
  2. Load the model and set it to evaluation mode.
  3. Check which classes it can detect — per the documentation, 90 object categories plus a background class (10 are labeled NA); "car" and "traffic light" are included. A helper function reads the metadata to retrieve categories.
  4. Load the image; specify target classes (e.g., "car", "traffic light") and box colors (red for cars, blue for traffic lights).
  5. Define detect_and_draw_bboxes(), which converts the image to a tensor, runs it through the model, and retrieves predictions with box coordinates, class labels, and confidence scores.
  6. Filter predictions to those matching the target classes above a confidence threshold; overlay the filtered boxes with draw_bounding_boxes, customizing line color and thickness. Return the annotated tensor (or the original image if nothing was detected above threshold).
  7. Call detect_and_draw_bboxes() with the model, image path, target category indices, box colors, and a confidence threshold of 0.7.
load_the_detector.py
# Load a pre-trained object detection model and set to evaluation mode
bb_model_weights = tv_models.detection.FasterRCNN_ResNet50_FPN_Weights.DEFAULT
bb_model = tv_models.detection.fasterrcnn_resnet50_fpn(weights=bb_model_weights).eval()

# Use the helper function to inspect the weights object of the object detection model.
num_classes, classes = get_model_classes_from_weights_meta(
    model=bb_model,
    weights_obj=bb_model_weights
)
target_classes.py
# Define a list of target classes to detect
target_class_names = ['car', 'traffic light']

# Define a corresponding list of colors for each class's bounding box
bbox_colors = ['red', 'blue']

# Use a list comprehension to get a list of all target indices
object_indices = [classes.index(name) for name in target_class_names]
detect_and_draw_bboxes.py
def detect_and_draw_bboxes(model, image_path, object_indices, labels, bbox_colors,
                           threshold, bbox_width=3):

    # Open and transform the image, and prepare the result tensor
    pil_image = Image.open(image_path).convert("RGB")
    transform_to_tensor = transforms.Compose([transforms.ToTensor()])
    tensor_image_batch = transform_to_tensor(pil_image).unsqueeze(0)
    result_image_tensor = (tensor_image_batch.squeeze(0) * 255).byte()

    # Perform inference to get predictions for all possible objects
    with torch.no_grad():
        prediction = model(tensor_image_batch)[0]

    # Initialize lists to collect all boxes, labels, and colors that meet the criteria
    all_boxes_to_draw = []
    all_labels_to_draw = []
    all_colors_to_draw = []

    # Loop through each target class to find its boxes
    for index, label, color in zip(object_indices, labels, bbox_colors):
        # Filter predictions for the current class index and confidence threshold
        class_mask = (prediction['labels'] == index) & (prediction['scores'] > threshold)

        # Get the boxes for the current class
        boxes_for_this_class = prediction['boxes'][class_mask]

        if boxes_for_this_class.nelement() > 0:
            # Add the found boxes to our master list
            all_boxes_to_draw.extend(boxes_for_this_class.tolist())
            # Create and add corresponding labels and colors
            all_labels_to_draw.extend([label] * len(boxes_for_this_class))
            all_colors_to_draw.extend([color] * len(boxes_for_this_class))

    # After checking all classes, draw all collected boxes at once if any were found
    if all_boxes_to_draw:
        result_image_tensor = vutils.draw_bounding_boxes(
            result_image_tensor,
            torch.tensor(all_boxes_to_draw),
            labels=all_labels_to_draw,
            colors=all_colors_to_draw,
            width=bbox_width
        )
    else:
        # If the list of boxes to draw is empty, print this information.
        print(f"No objects from the list {labels} were found with a confidence score above {threshold}.\n")

    return result_image_tensor
run_detection.py
confidence_threshold = 0.7

# Execute the main detection function
result_image_tensor = detect_and_draw_bboxes(
    model=bb_model,                    # The pre-trained object detection model.
    image_path=image_path,             # The path to the input image.
    object_indices=object_indices,     # The list of integer indices for the target classes.
    labels=target_class_names,         # The list of string names for the box labels.
    bbox_colors=bbox_colors,           # The list of colors for the bounding boxes.
    threshold=confidence_threshold,    # The minimum confidence score for a detection.
)
Street scene under a stone bridge with twelve parked and moving cars each outlined by a red bounding box labelled car
output of detect_and_draw_bboxes() — Faster R-CNN R50-FPN, COCO weights, threshold 0.70: 12 boxes for car, none for traffic light (there are none in frame)
DATASETS

Use FakeData To Test Or Debug

fake_data.py — what changes
from torchvision.datasets import FakeData
ds = FakeData(size=8, image_size=(3, 32, 32), transform=transforms.ToTensor())
WRAPPING UP

To Summarize

raw imagesinspected, trained model
  1. 01 Tools
  2. 02 Preprocessing &
    Augmentation
  3. 03 Datasets
  4. 04 Pretrained Models
    & Transfer Learning
  5. 05 Visualization