We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
bounding boxes in torchvision.utils
## 🚀 Feature

I'd like to easily be able to draw a bounding box onto an image represented as a torch tensor. 

## Motivation

Using YOLO, I get a bunch of bounding boxes in an image. I want to be able to easily draw those onto a torch tensor (created via `torchvision.transforms.ToTensor`). This seems like a reasonable request because other bbox utilities such as NMS exist in torchvision.

## Pitch

```python
tensor_image = torch.tensor(...)
new_img = torchvision.utils.draw_bounding_box(
    tensor_image,
    [x_min, y_min, x_max, y_max],
)
```

## Alternatives

I *think* the following works right now, but the `torch.Tensor` > `PIL.Image` > `torch.Tensor` conversion for a single operation per image makes me feel uncomfortable due to efficiency.
```python
# entirely untested!

tensor_image = torch.tensor(...)
def draw_bounding_box(tensor, bbox, **kwargs):  # TODO: don't use a kwargs dict
    # adapted from https://pytorch.org/docs/stable/_modules/torchvision/utils.html#save_image
    from PIL import Image
    # TODO: none of the kwargs to make_grid are in this scope
    grid = make_grid(tensor, nrow=nrow, padding=padding, pad_value=pad_value,
                     normalize=normalize, range=range, scale_each=scale_each)
    # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer
    ndarr = grid.mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to('cpu', torch.uint8).numpy()

    # TODO: convert bbox to the appropriate type
    return cv2.rectangle(ndarr, bbox)  # use PIL if opencv-python isn't a dependency
```

## Additional context

See [`cv2.rectangle`](https://docs.opencv.org/4.4.0/d6/d6e/group__imgproc__draw.html#ga07d2f74cadcf8e305e810ce8eed13bc9) ([source](https://github.com/opencv/opencv/blob/422f802ec96feecbc9db2f60879e8fe364cf8a92/modules/imgproc/src/drawing.cpp#L1829-L1859)) and [`PIL.ImageDraw.rectangle`](https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#PIL.ImageDraw.ImageDraw.rectangle) ([source](https://pillow.readthedocs.io/en/stable/_modules/PIL/ImageDraw.html#ImageDraw.rectangle)).


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `torchvision/utils.py`
Content:
```
1 from typing import Union, Optional, List, Tuple, Text, BinaryIO
2 import io
3 import pathlib
4 import torch
5 import math
6 irange = range
7 
8 
9 def make_grid(
10     tensor: Union[torch.Tensor, List[torch.Tensor]],
11     nrow: int = 8,
12     padding: int = 2,
13     normalize: bool = False,
14     range: Optional[Tuple[int, int]] = None,
15     scale_each: bool = False,
16     pad_value: int = 0,
17 ) -> torch.Tensor:
18     """Make a grid of images.
19 
20     Args:
21         tensor (Tensor or list): 4D mini-batch Tensor of shape (B x C x H x W)
22             or a list of images all of the same size.
23         nrow (int, optional): Number of images displayed in each row of the grid.
24             The final grid size is ``(B / nrow, nrow)``. Default: ``8``.
25         padding (int, optional): amount of padding. Default: ``2``.
26         normalize (bool, optional): If True, shift the image to the range (0, 1),
27             by the min and max values specified by :attr:`range`. Default: ``False``.
28         range (tuple, optional): tuple (min, max) where min and max are numbers,
29             then these numbers are used to normalize the image. By default, min and max
30             are computed from the tensor.
31         scale_each (bool, optional): If ``True``, scale each image in the batch of
32             images separately rather than the (min, max) over all images. Default: ``False``.
33         pad_value (float, optional): Value for the padded pixels. Default: ``0``.
34 
35     Example:
36         See this notebook `here <https://gist.github.com/anonymous/bf16430f7750c023141c562f3e9f2a91>`_
37 
38     """
39     if not (torch.is_tensor(tensor) or
40             (isinstance(tensor, list) and all(torch.is_tensor(t) for t in tensor))):
41         raise TypeError('tensor or list of tensors expected, got {}'.format(type(tensor)))
42 
43     # if list of tensors, convert to a 4D mini-batch Tensor
44     if isinstance(tensor, list):
45         tensor = torch.stack(tensor, dim=0)
46 
47     if tensor.dim() == 2:  # single image H x W
48         tensor = tensor.unsqueeze(0)
49     if tensor.dim() == 3:  # single image
50         if tensor.size(0) == 1:  # if single-channel, convert to 3-channel
51             tensor = torch.cat((tensor, tensor, tensor), 0)
52         tensor = tensor.unsqueeze(0)
53 
54     if tensor.dim() == 4 and tensor.size(1) == 1:  # single-channel images
55         tensor = torch.cat((tensor, tensor, tensor), 1)
56 
57     if normalize is True:
58         tensor = tensor.clone()  # avoid modifying tensor in-place
59         if range is not None:
60             assert isinstance(range, tuple), \
61                 "range has to be a tuple (min, max) if specified. min and max are numbers"
62 
63         def norm_ip(img, low, high):
64             img.clamp_(min=low, max=high)
65             img.sub_(low).div_(max(high - low, 1e-5))
66 
67         def norm_range(t, range):
68             if range is not None:
69                 norm_ip(t, range[0], range[1])
70             else:
71                 norm_ip(t, float(t.min()), float(t.max()))
72 
73         if scale_each is True:
74             for t in tensor:  # loop over mini-batch dimension
75                 norm_range(t, range)
76         else:
77             norm_range(tensor, range)
78 
79     if tensor.size(0) == 1:
80         return tensor.squeeze(0)
81 
82     # make the mini-batch of images into a grid
83     nmaps = tensor.size(0)
84     xmaps = min(nrow, nmaps)
85     ymaps = int(math.ceil(float(nmaps) / xmaps))
86     height, width = int(tensor.size(2) + padding), int(tensor.size(3) + padding)
87     num_channels = tensor.size(1)
88     grid = tensor.new_full((num_channels, height * ymaps + padding, width * xmaps + padding), pad_value)
89     k = 0
90     for y in irange(ymaps):
91         for x in irange(xmaps):
92             if k >= nmaps:
93                 break
94             # Tensor.copy_() is a valid method but seems to be missing from the stubs
95             # https://pytorch.org/docs/stable/tensors.html#torch.Tensor.copy_
96             grid.narrow(1, y * height + padding, height - padding).narrow(  # type: ignore[attr-defined]
97                 2, x * width + padding, width - padding
98             ).copy_(tensor[k])
99             k = k + 1
100     return grid
101 
102 
103 def save_image(
104     tensor: Union[torch.Tensor, List[torch.Tensor]],
105     fp: Union[Text, pathlib.Path, BinaryIO],
106     nrow: int = 8,
107     padding: int = 2,
108     normalize: bool = False,
109     range: Optional[Tuple[int, int]] = None,
110     scale_each: bool = False,
111     pad_value: int = 0,
112     format: Optional[str] = None,
113 ) -> None:
114     """Save a given Tensor into an image file.
115 
116     Args:
117         tensor (Tensor or list): Image to be saved. If given a mini-batch tensor,
118             saves the tensor as a grid of images by calling ``make_grid``.
119         fp (string or file object): A filename or a file object
120         format(Optional):  If omitted, the format to use is determined from the filename extension.
121             If a file object was used instead of a filename, this parameter should always be used.
122         **kwargs: Other arguments are documented in ``make_grid``.
123     """
124     from PIL import Image
125     grid = make_grid(tensor, nrow=nrow, padding=padding, pad_value=pad_value,
126                      normalize=normalize, range=range, scale_each=scale_each)
127     # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer
128     ndarr = grid.mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to('cpu', torch.uint8).numpy()
129     im = Image.fromarray(ndarr)
130     im.save(fp, format=format)
131
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/torchvision/utils.py b/torchvision/utils.py
--- a/torchvision/utils.py
+++ b/torchvision/utils.py
@@ -1,8 +1,13 @@
 from typing import Union, Optional, List, Tuple, Text, BinaryIO
-import io
 import pathlib
 import torch
 import math
+import numpy as np
+from PIL import Image, ImageDraw
+from PIL import ImageFont
+
+__all__ = ["make_grid", "save_image", "draw_bounding_boxes"]
+
 irange = range
 
 
@@ -121,10 +126,64 @@
             If a file object was used instead of a filename, this parameter should always be used.
         **kwargs: Other arguments are documented in ``make_grid``.
     """
-    from PIL import Image
     grid = make_grid(tensor, nrow=nrow, padding=padding, pad_value=pad_value,
                      normalize=normalize, range=range, scale_each=scale_each)
     # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer
     ndarr = grid.mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to('cpu', torch.uint8).numpy()
     im = Image.fromarray(ndarr)
     im.save(fp, format=format)
+
+
+@torch.no_grad()
+def draw_bounding_boxes(
+    image: torch.Tensor,
+    boxes: torch.Tensor,
+    labels: Optional[List[str]] = None,
+    colors: Optional[List[Union[str, Tuple[int, int, int]]]] = None,
+    width: int = 1,
+    font: Optional[str] = None,
+    font_size: int = 10
+) -> torch.Tensor:
+
+    """
+    Draws bounding boxes on given image.
+    The values of the input image should be uint8 between 0 and 255.
+
+    Args:
+        image (Tensor): Tensor of shape (C x H x W)
+        bboxes (Tensor): Tensor of size (N, 4) containing bounding boxes in (xmin, ymin, xmax, ymax) format. Note that
+            the boxes are absolute coordinates with respect to the image. In other words: `0 <= xmin < xmax < W` and
+            `0 <= ymin < ymax < H`.
+        labels (List[str]): List containing the labels of bounding boxes.
+        colors (List[Union[str, Tuple[int, int, int]]]): List containing the colors of bounding boxes. The colors can
+            be represented as `str` or `Tuple[int, int, int]`.
+        width (int): Width of bounding box.
+        font (str): A filename containing a TrueType font. If the file is not found in this filename, the loader may
+            also search in other directories, such as the `fonts/` directory on Windows or `/Library/Fonts/`,
+            `/System/Library/Fonts/` and `~/Library/Fonts/` on macOS.
+        font_size (int): The requested font size in points.
+    """
+
+    if not isinstance(image, torch.Tensor):
+        raise TypeError(f"Tensor expected, got {type(image)}")
+    elif image.dtype != torch.uint8:
+        raise ValueError(f"Tensor uint8 expected, got {image.dtype}")
+    elif image.dim() != 3:
+        raise ValueError("Pass individual images, not batches")
+
+    ndarr = image.permute(1, 2, 0).numpy()
+    img_to_draw = Image.fromarray(ndarr)
+
+    img_boxes = boxes.to(torch.int64).tolist()
+
+    draw = ImageDraw.Draw(img_to_draw)
+
+    for i, bbox in enumerate(img_boxes):
+        color = None if colors is None else colors[i]
+        draw.rectangle(bbox, width=width, outline=color)
+
+        if labels is not None:
+            txt_font = ImageFont.load_default() if font is None else ImageFont.truetype(font=font, size=font_size)
+            draw.text((bbox[0], bbox[1]), labels[i], fill=color, font=txt_font)
+
+    return torch.from_numpy(np.array(img_to_draw)).permute(2, 0, 1)

{"golden_diff": "diff --git a/torchvision/utils.py b/torchvision/utils.py\n--- a/torchvision/utils.py\n+++ b/torchvision/utils.py\n@@ -1,8 +1,13 @@\n from typing import Union, Optional, List, Tuple, Text, BinaryIO\n-import io\n import pathlib\n import torch\n import math\n+import numpy as np\n+from PIL import Image, ImageDraw\n+from PIL import ImageFont\n+\n+__all__ = [\"make_grid\", \"save_image\", \"draw_bounding_boxes\"]\n+\n irange = range\n \n \n@@ -121,10 +126,64 @@\n             If a file object was used instead of a filename, this parameter should always be used.\n         **kwargs: Other arguments are documented in ``make_grid``.\n     \"\"\"\n-    from PIL import Image\n     grid = make_grid(tensor, nrow=nrow, padding=padding, pad_value=pad_value,\n                      normalize=normalize, range=range, scale_each=scale_each)\n     # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer\n     ndarr = grid.mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to('cpu', torch.uint8).numpy()\n     im = Image.fromarray(ndarr)\n     im.save(fp, format=format)\n+\n+\n+@torch.no_grad()\n+def draw_bounding_boxes(\n+    image: torch.Tensor,\n+    boxes: torch.Tensor,\n+    labels: Optional[List[str]] = None,\n+    colors: Optional[List[Union[str, Tuple[int, int, int]]]] = None,\n+    width: int = 1,\n+    font: Optional[str] = None,\n+    font_size: int = 10\n+) -> torch.Tensor:\n+\n+    \"\"\"\n+    Draws bounding boxes on given image.\n+    The values of the input image should be uint8 between 0 and 255.\n+\n+    Args:\n+        image (Tensor): Tensor of shape (C x H x W)\n+        bboxes (Tensor): Tensor of size (N, 4) containing bounding boxes in (xmin, ymin, xmax, ymax) format. Note that\n+            the boxes are absolute coordinates with respect to the image. In other words: `0 <= xmin < xmax < W` and\n+            `0 <= ymin < ymax < H`.\n+        labels (List[str]): List containing the labels of bounding boxes.\n+        colors (List[Union[str, Tuple[int, int, int]]]): List containing the colors of bounding boxes. The colors can\n+            be represented as `str` or `Tuple[int, int, int]`.\n+        width (int): Width of bounding box.\n+        font (str): A filename containing a TrueType font. If the file is not found in this filename, the loader may\n+            also search in other directories, such as the `fonts/` directory on Windows or `/Library/Fonts/`,\n+            `/System/Library/Fonts/` and `~/Library/Fonts/` on macOS.\n+        font_size (int): The requested font size in points.\n+    \"\"\"\n+\n+    if not isinstance(image, torch.Tensor):\n+        raise TypeError(f\"Tensor expected, got {type(image)}\")\n+    elif image.dtype != torch.uint8:\n+        raise ValueError(f\"Tensor uint8 expected, got {image.dtype}\")\n+    elif image.dim() != 3:\n+        raise ValueError(\"Pass individual images, not batches\")\n+\n+    ndarr = image.permute(1, 2, 0).numpy()\n+    img_to_draw = Image.fromarray(ndarr)\n+\n+    img_boxes = boxes.to(torch.int64).tolist()\n+\n+    draw = ImageDraw.Draw(img_to_draw)\n+\n+    for i, bbox in enumerate(img_boxes):\n+        color = None if colors is None else colors[i]\n+        draw.rectangle(bbox, width=width, outline=color)\n+\n+        if labels is not None:\n+            txt_font = ImageFont.load_default() if font is None else ImageFont.truetype(font=font, size=font_size)\n+            draw.text((bbox[0], bbox[1]), labels[i], fill=color, font=txt_font)\n+\n+    return torch.from_numpy(np.array(img_to_draw)).permute(2, 0, 1)\n", "issue": "bounding boxes in torchvision.utils\n## \ud83d\ude80 Feature\r\n\r\nI'd like to easily be able to draw a bounding box onto an image represented as a torch tensor. \r\n\r\n## Motivation\r\n\r\nUsing YOLO, I get a bunch of bounding boxes in an image. I want to be able to easily draw those onto a torch tensor (created via `torchvision.transforms.ToTensor`). This seems like a reasonable request because other bbox utilities such as NMS exist in torchvision.\r\n\r\n## Pitch\r\n\r\n```python\r\ntensor_image = torch.tensor(...)\r\nnew_img = torchvision.utils.draw_bounding_box(\r\n    tensor_image,\r\n    [x_min, y_min, x_max, y_max],\r\n)\r\n```\r\n\r\n## Alternatives\r\n\r\nI *think* the following works right now, but the `torch.Tensor` > `PIL.Image` > `torch.Tensor` conversion for a single operation per image makes me feel uncomfortable due to efficiency.\r\n```python\r\n# entirely untested!\r\n\r\ntensor_image = torch.tensor(...)\r\ndef draw_bounding_box(tensor, bbox, **kwargs):  # TODO: don't use a kwargs dict\r\n    # adapted from https://pytorch.org/docs/stable/_modules/torchvision/utils.html#save_image\r\n    from PIL import Image\r\n    # TODO: none of the kwargs to make_grid are in this scope\r\n    grid = make_grid(tensor, nrow=nrow, padding=padding, pad_value=pad_value,\r\n                     normalize=normalize, range=range, scale_each=scale_each)\r\n    # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer\r\n    ndarr = grid.mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to('cpu', torch.uint8).numpy()\r\n\r\n    # TODO: convert bbox to the appropriate type\r\n    return cv2.rectangle(ndarr, bbox)  # use PIL if opencv-python isn't a dependency\r\n```\r\n\r\n## Additional context\r\n\r\nSee [`cv2.rectangle`](https://docs.opencv.org/4.4.0/d6/d6e/group__imgproc__draw.html#ga07d2f74cadcf8e305e810ce8eed13bc9) ([source](https://github.com/opencv/opencv/blob/422f802ec96feecbc9db2f60879e8fe364cf8a92/modules/imgproc/src/drawing.cpp#L1829-L1859)) and [`PIL.ImageDraw.rectangle`](https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#PIL.ImageDraw.ImageDraw.rectangle) ([source](https://pillow.readthedocs.io/en/stable/_modules/PIL/ImageDraw.html#ImageDraw.rectangle)).\r\n\n", "before_files": [{"content": "from typing import Union, Optional, List, Tuple, Text, BinaryIO\nimport io\nimport pathlib\nimport torch\nimport math\nirange = range\n\n\ndef make_grid(\n    tensor: Union[torch.Tensor, List[torch.Tensor]],\n    nrow: int = 8,\n    padding: int = 2,\n    normalize: bool = False,\n    range: Optional[Tuple[int, int]] = None,\n    scale_each: bool = False,\n    pad_value: int = 0,\n) -> torch.Tensor:\n    \"\"\"Make a grid of images.\n\n    Args:\n        tensor (Tensor or list): 4D mini-batch Tensor of shape (B x C x H x W)\n            or a list of images all of the same size.\n        nrow (int, optional): Number of images displayed in each row of the grid.\n            The final grid size is ``(B / nrow, nrow)``. Default: ``8``.\n        padding (int, optional): amount of padding. Default: ``2``.\n        normalize (bool, optional): If True, shift the image to the range (0, 1),\n            by the min and max values specified by :attr:`range`. Default: ``False``.\n        range (tuple, optional): tuple (min, max) where min and max are numbers,\n            then these numbers are used to normalize the image. By default, min and max\n            are computed from the tensor.\n        scale_each (bool, optional): If ``True``, scale each image in the batch of\n            images separately rather than the (min, max) over all images. Default: ``False``.\n        pad_value (float, optional): Value for the padded pixels. Default: ``0``.\n\n    Example:\n        See this notebook `here <https://gist.github.com/anonymous/bf16430f7750c023141c562f3e9f2a91>`_\n\n    \"\"\"\n    if not (torch.is_tensor(tensor) or\n            (isinstance(tensor, list) and all(torch.is_tensor(t) for t in tensor))):\n        raise TypeError('tensor or list of tensors expected, got {}'.format(type(tensor)))\n\n    # if list of tensors, convert to a 4D mini-batch Tensor\n    if isinstance(tensor, list):\n        tensor = torch.stack(tensor, dim=0)\n\n    if tensor.dim() == 2:  # single image H x W\n        tensor = tensor.unsqueeze(0)\n    if tensor.dim() == 3:  # single image\n        if tensor.size(0) == 1:  # if single-channel, convert to 3-channel\n            tensor = torch.cat((tensor, tensor, tensor), 0)\n        tensor = tensor.unsqueeze(0)\n\n    if tensor.dim() == 4 and tensor.size(1) == 1:  # single-channel images\n        tensor = torch.cat((tensor, tensor, tensor), 1)\n\n    if normalize is True:\n        tensor = tensor.clone()  # avoid modifying tensor in-place\n        if range is not None:\n            assert isinstance(range, tuple), \\\n                \"range has to be a tuple (min, max) if specified. min and max are numbers\"\n\n        def norm_ip(img, low, high):\n            img.clamp_(min=low, max=high)\n            img.sub_(low).div_(max(high - low, 1e-5))\n\n        def norm_range(t, range):\n            if range is not None:\n                norm_ip(t, range[0], range[1])\n            else:\n                norm_ip(t, float(t.min()), float(t.max()))\n\n        if scale_each is True:\n            for t in tensor:  # loop over mini-batch dimension\n                norm_range(t, range)\n        else:\n            norm_range(tensor, range)\n\n    if tensor.size(0) == 1:\n        return tensor.squeeze(0)\n\n    # make the mini-batch of images into a grid\n    nmaps = tensor.size(0)\n    xmaps = min(nrow, nmaps)\n    ymaps = int(math.ceil(float(nmaps) / xmaps))\n    height, width = int(tensor.size(2) + padding), int(tensor.size(3) + padding)\n    num_channels = tensor.size(1)\n    grid = tensor.new_full((num_channels, height * ymaps + padding, width * xmaps + padding), pad_value)\n    k = 0\n    for y in irange(ymaps):\n        for x in irange(xmaps):\n            if k >= nmaps:\n                break\n            # Tensor.copy_() is a valid method but seems to be missing from the stubs\n            # https://pytorch.org/docs/stable/tensors.html#torch.Tensor.copy_\n            grid.narrow(1, y * height + padding, height - padding).narrow(  # type: ignore[attr-defined]\n                2, x * width + padding, width - padding\n            ).copy_(tensor[k])\n            k = k + 1\n    return grid\n\n\ndef save_image(\n    tensor: Union[torch.Tensor, List[torch.Tensor]],\n    fp: Union[Text, pathlib.Path, BinaryIO],\n    nrow: int = 8,\n    padding: int = 2,\n    normalize: bool = False,\n    range: Optional[Tuple[int, int]] = None,\n    scale_each: bool = False,\n    pad_value: int = 0,\n    format: Optional[str] = None,\n) -> None:\n    \"\"\"Save a given Tensor into an image file.\n\n    Args:\n        tensor (Tensor or list): Image to be saved. If given a mini-batch tensor,\n            saves the tensor as a grid of images by calling ``make_grid``.\n        fp (string or file object): A filename or a file object\n        format(Optional):  If omitted, the format to use is determined from the filename extension.\n            If a file object was used instead of a filename, this parameter should always be used.\n        **kwargs: Other arguments are documented in ``make_grid``.\n    \"\"\"\n    from PIL import Image\n    grid = make_grid(tensor, nrow=nrow, padding=padding, pad_value=pad_value,\n                     normalize=normalize, range=range, scale_each=scale_each)\n    # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer\n    ndarr = grid.mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to('cpu', torch.uint8).numpy()\n    im = Image.fromarray(ndarr)\n    im.save(fp, format=format)\n", "path": "torchvision/utils.py"}], "after_files": [{"content": "from typing import Union, Optional, List, Tuple, Text, BinaryIO\nimport pathlib\nimport torch\nimport math\nimport numpy as np\nfrom PIL import Image, ImageDraw\nfrom PIL import ImageFont\n\n__all__ = [\"make_grid\", \"save_image\", \"draw_bounding_boxes\"]\n\nirange = range\n\n\ndef make_grid(\n    tensor: Union[torch.Tensor, List[torch.Tensor]],\n    nrow: int = 8,\n    padding: int = 2,\n    normalize: bool = False,\n    range: Optional[Tuple[int, int]] = None,\n    scale_each: bool = False,\n    pad_value: int = 0,\n) -> torch.Tensor:\n    \"\"\"Make a grid of images.\n\n    Args:\n        tensor (Tensor or list): 4D mini-batch Tensor of shape (B x C x H x W)\n            or a list of images all of the same size.\n        nrow (int, optional): Number of images displayed in each row of the grid.\n            The final grid size is ``(B / nrow, nrow)``. Default: ``8``.\n        padding (int, optional): amount of padding. Default: ``2``.\n        normalize (bool, optional): If True, shift the image to the range (0, 1),\n            by the min and max values specified by :attr:`range`. Default: ``False``.\n        range (tuple, optional): tuple (min, max) where min and max are numbers,\n            then these numbers are used to normalize the image. By default, min and max\n            are computed from the tensor.\n        scale_each (bool, optional): If ``True``, scale each image in the batch of\n            images separately rather than the (min, max) over all images. Default: ``False``.\n        pad_value (float, optional): Value for the padded pixels. Default: ``0``.\n\n    Example:\n        See this notebook `here <https://gist.github.com/anonymous/bf16430f7750c023141c562f3e9f2a91>`_\n\n    \"\"\"\n    if not (torch.is_tensor(tensor) or\n            (isinstance(tensor, list) and all(torch.is_tensor(t) for t in tensor))):\n        raise TypeError('tensor or list of tensors expected, got {}'.format(type(tensor)))\n\n    # if list of tensors, convert to a 4D mini-batch Tensor\n    if isinstance(tensor, list):\n        tensor = torch.stack(tensor, dim=0)\n\n    if tensor.dim() == 2:  # single image H x W\n        tensor = tensor.unsqueeze(0)\n    if tensor.dim() == 3:  # single image\n        if tensor.size(0) == 1:  # if single-channel, convert to 3-channel\n            tensor = torch.cat((tensor, tensor, tensor), 0)\n        tensor = tensor.unsqueeze(0)\n\n    if tensor.dim() == 4 and tensor.size(1) == 1:  # single-channel images\n        tensor = torch.cat((tensor, tensor, tensor), 1)\n\n    if normalize is True:\n        tensor = tensor.clone()  # avoid modifying tensor in-place\n        if range is not None:\n            assert isinstance(range, tuple), \\\n                \"range has to be a tuple (min, max) if specified. min and max are numbers\"\n\n        def norm_ip(img, low, high):\n            img.clamp_(min=low, max=high)\n            img.sub_(low).div_(max(high - low, 1e-5))\n\n        def norm_range(t, range):\n            if range is not None:\n                norm_ip(t, range[0], range[1])\n            else:\n                norm_ip(t, float(t.min()), float(t.max()))\n\n        if scale_each is True:\n            for t in tensor:  # loop over mini-batch dimension\n                norm_range(t, range)\n        else:\n            norm_range(tensor, range)\n\n    if tensor.size(0) == 1:\n        return tensor.squeeze(0)\n\n    # make the mini-batch of images into a grid\n    nmaps = tensor.size(0)\n    xmaps = min(nrow, nmaps)\n    ymaps = int(math.ceil(float(nmaps) / xmaps))\n    height, width = int(tensor.size(2) + padding), int(tensor.size(3) + padding)\n    num_channels = tensor.size(1)\n    grid = tensor.new_full((num_channels, height * ymaps + padding, width * xmaps + padding), pad_value)\n    k = 0\n    for y in irange(ymaps):\n        for x in irange(xmaps):\n            if k >= nmaps:\n                break\n            # Tensor.copy_() is a valid method but seems to be missing from the stubs\n            # https://pytorch.org/docs/stable/tensors.html#torch.Tensor.copy_\n            grid.narrow(1, y * height + padding, height - padding).narrow(  # type: ignore[attr-defined]\n                2, x * width + padding, width - padding\n            ).copy_(tensor[k])\n            k = k + 1\n    return grid\n\n\ndef save_image(\n    tensor: Union[torch.Tensor, List[torch.Tensor]],\n    fp: Union[Text, pathlib.Path, BinaryIO],\n    nrow: int = 8,\n    padding: int = 2,\n    normalize: bool = False,\n    range: Optional[Tuple[int, int]] = None,\n    scale_each: bool = False,\n    pad_value: int = 0,\n    format: Optional[str] = None,\n) -> None:\n    \"\"\"Save a given Tensor into an image file.\n\n    Args:\n        tensor (Tensor or list): Image to be saved. If given a mini-batch tensor,\n            saves the tensor as a grid of images by calling ``make_grid``.\n        fp (string or file object): A filename or a file object\n        format(Optional):  If omitted, the format to use is determined from the filename extension.\n            If a file object was used instead of a filename, this parameter should always be used.\n        **kwargs: Other arguments are documented in ``make_grid``.\n    \"\"\"\n    grid = make_grid(tensor, nrow=nrow, padding=padding, pad_value=pad_value,\n                     normalize=normalize, range=range, scale_each=scale_each)\n    # Add 0.5 after unnormalizing to [0, 255] to round to nearest integer\n    ndarr = grid.mul(255).add_(0.5).clamp_(0, 255).permute(1, 2, 0).to('cpu', torch.uint8).numpy()\n    im = Image.fromarray(ndarr)\n    im.save(fp, format=format)\n\n\n@torch.no_grad()\ndef draw_bounding_boxes(\n    image: torch.Tensor,\n    boxes: torch.Tensor,\n    labels: Optional[List[str]] = None,\n    colors: Optional[List[Union[str, Tuple[int, int, int]]]] = None,\n    width: int = 1,\n    font: Optional[str] = None,\n    font_size: int = 10\n) -> torch.Tensor:\n\n    \"\"\"\n    Draws bounding boxes on given image.\n    The values of the input image should be uint8 between 0 and 255.\n\n    Args:\n        image (Tensor): Tensor of shape (C x H x W)\n        bboxes (Tensor): Tensor of size (N, 4) containing bounding boxes in (xmin, ymin, xmax, ymax) format. Note that\n            the boxes are absolute coordinates with respect to the image. In other words: `0 <= xmin < xmax < W` and\n            `0 <= ymin < ymax < H`.\n        labels (List[str]): List containing the labels of bounding boxes.\n        colors (List[Union[str, Tuple[int, int, int]]]): List containing the colors of bounding boxes. The colors can\n            be represented as `str` or `Tuple[int, int, int]`.\n        width (int): Width of bounding box.\n        font (str): A filename containing a TrueType font. If the file is not found in this filename, the loader may\n            also search in other directories, such as the `fonts/` directory on Windows or `/Library/Fonts/`,\n            `/System/Library/Fonts/` and `~/Library/Fonts/` on macOS.\n        font_size (int): The requested font size in points.\n    \"\"\"\n\n    if not isinstance(image, torch.Tensor):\n        raise TypeError(f\"Tensor expected, got {type(image)}\")\n    elif image.dtype != torch.uint8:\n        raise ValueError(f\"Tensor uint8 expected, got {image.dtype}\")\n    elif image.dim() != 3:\n        raise ValueError(\"Pass individual images, not batches\")\n\n    ndarr = image.permute(1, 2, 0).numpy()\n    img_to_draw = Image.fromarray(ndarr)\n\n    img_boxes = boxes.to(torch.int64).tolist()\n\n    draw = ImageDraw.Draw(img_to_draw)\n\n    for i, bbox in enumerate(img_boxes):\n        color = None if colors is None else colors[i]\n        draw.rectangle(bbox, width=width, outline=color)\n\n        if labels is not None:\n            txt_font = ImageFont.load_default() if font is None else ImageFont.truetype(font=font, size=font_size)\n            draw.text((bbox[0], bbox[1]), labels[i], fill=color, font=txt_font)\n\n    return torch.from_numpy(np.array(img_to_draw)).permute(2, 0, 1)\n", "path": "torchvision/utils.py"}]}