Repository: pytorch/vision

Issue:
Add fill parameter to utils.draw_bounding_boxes.
## 🚀 Feature

Fill parameter allows creating a semi-transparent box. This is particularly useful for Mask RCNN Model.
This would complete utils for Object detection and Instance Segmentation (least with rectangular boxes)

## Motivation

In Instance segmentation models, we also care about masks, not just the bounding box. Fill parameter allows us to fill in a semi-transparent way. Also, this parameter is optional hence it does not affect performance.

## Pitch

Add a param fill as follows
```
fill: Optional[List[Union[str, Tuple[int, int, int]]]] = None,

```

Here is complete running code with a few edits

```
@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,
    fill: 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]`.
        fill: Optional[List[Union[str, Tuple[int, int, int]]]] = None,
        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, "RGBA")

    txt_font = ImageFont.load_default() if font is None else ImageFont.truetype(font=font, size=font_size)

    for i, bbox in enumerate(img_boxes):
        color = None if colors is None else colors[i]
        draw.rectangle(bbox, width=width, outline=color, fill=fill)

        if labels is not None:
            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)
```

This makes mask RCNN output more clear, and people can play with fill parameter such as confidence based fill, fill with colors different per class, etc.

## Additional context

I can send PR for this :sweat_smile:  I'm attaching outputs of above code.

![draw_boxes_util2](https://user-images.githubusercontent.com/47158509/105579984-b0b14680-5daf-11eb-89fa-9c525608dcca.png)

(Sorry PyTorch logo :pray: )

![draw_boxes_util](https://user-images.githubusercontent.com/47158509/105579987-b3ac3700-5daf-11eb-9280-8390f86721b5.png)




Discussion:
I like the idea of filling the box with colour but do you think it's necessary to use the a different colour than the border? Perhas that can be reused and instead turn fill to a boolean? What do you think?
It could be fine with boolean too. 
Just wondering if user would like dark black (distinct color for outline) and fill with semi-transparent color. 

I'm not sure if the above is really valid use case. 

One such example is here.

![draw_boxes_util4](https://user-images.githubusercontent.com/47158509/105827714-55b66400-5fe8-11eb-9010-ac967d9e628e.png)

Code to reproduce results

```
image = torch.ones(3, 224, 224)
boxes = torch.tensor([[12, 23, 40, 40], [20, 30, 50, 50], [20, 50, 120, 120]])
image = torchvision.transforms.ConvertImageDtype(torch.uint8)(image)
result = draw_bounding_boxes(image=image, boxes=boxes, fill=(50, 100, 100, 127), colors=["black", "black", "black"])
res = Image.fromarray(result.permute(1, 2, 0).contiguous().numpy())
res.save("draw_boxes_util4.png")
```


Let me know I can send PR for either. 

Only caveat is since we are drawing semi transparent color mask using Alpha channel (4 channel image)
Users need to save returned tensor as PNG format not jpg. I think we can add that in documentation. 

I think we can leave the option open to user? IMO We can easily keep flexibility



Resolving patch:
diff --git a/torchvision/utils.py b/torchvision/utils.py
--- a/torchvision/utils.py
+++ b/torchvision/utils.py
@@ -4,8 +4,7 @@
 import math
 import warnings
 import numpy as np
-from PIL import Image, ImageDraw
-from PIL import ImageFont
+from PIL import Image, ImageDraw, ImageFont, ImageColor
 
 __all__ = ["make_grid", "save_image", "draw_bounding_boxes"]
 
@@ -142,6 +141,7 @@ def draw_bounding_boxes(
     boxes: torch.Tensor,
     labels: Optional[List[str]] = None,
     colors: Optional[List[Union[str, Tuple[int, int, int]]]] = None,
+    fill: Optional[bool] = False,
     width: int = 1,
     font: Optional[str] = None,
     font_size: int = 10
@@ -150,6 +150,7 @@ def draw_bounding_boxes(
     """
     Draws bounding boxes on given image.
     The values of the input image should be uint8 between 0 and 255.
+    If filled, Resulting Tensor should be saved as PNG image.
 
     Args:
         image (Tensor): Tensor of shape (C x H x W)
@@ -159,6 +160,7 @@ def draw_bounding_boxes(
         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]`.
+        fill (bool): If `True` fills the bounding box with specified color.
         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/`,
@@ -178,12 +180,31 @@ def draw_bounding_boxes(
 
     img_boxes = boxes.to(torch.int64).tolist()
 
-    draw = ImageDraw.Draw(img_to_draw)
+    if fill:
+        draw = ImageDraw.Draw(img_to_draw, "RGBA")
+
+    else:
+        draw = ImageDraw.Draw(img_to_draw)
+
     txt_font = ImageFont.load_default() if font is None else ImageFont.truetype(font=font, size=font_size)
 
     for i, bbox in enumerate(img_boxes):
-        color = None if colors is None else colors[i]
-        draw.rectangle(bbox, width=width, outline=color)
+        if colors is None:
+            color = None
+        else:
+            color = colors[i]
+
+        if fill:
+            if color is None:
+                fill_color = (255, 255, 255, 100)
+            elif isinstance(color, str):
+                # This will automatically raise Error if rgb cannot be parsed.
+                fill_color = ImageColor.getrgb(color) + (100,)
+            elif isinstance(color, tuple):
+                fill_color = color + (100,)
+            draw.rectangle(bbox, width=width, outline=color, fill=fill_color)
+        else:
+            draw.rectangle(bbox, width=width, outline=color)
 
         if labels is not None:
             draw.text((bbox[0], bbox[1]), labels[i], fill=color, font=txt_font)


Test patch:
diff --git a/test/assets/fakedata/draw_boxes_util.png b/test/assets/fakedata/draw_boxes_util.png
Binary files a/test/assets/fakedata/draw_boxes_util.png and b/test/assets/fakedata/draw_boxes_util.png differ
diff --git a/test/test_utils.py b/test/test_utils.py
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -86,7 +86,7 @@ def test_draw_boxes(self):
                              [10, 15, 30, 35], [23, 35, 93, 95]], dtype=torch.float)
         labels = ["a", "b", "c", "d"]
         colors = ["green", "#FF00FF", (0, 255, 0), "red"]
-        result = utils.draw_bounding_boxes(img, boxes, labels=labels, colors=colors)
+        result = utils.draw_bounding_boxes(img, boxes, labels=labels, colors=colors, fill=True)
 
         path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "fakedata", "draw_boxes_util.png")
         if not os.path.exists(path):