Repository: pytorch/vision

Issue:
Generate colors from random color palette in draw_bounding_boxes
### 🚀 The feature

The current default white color looks bad. Maybe we can generate colors from palette as we do in drawing masks?

### Motivation, pitch

It should be simple tweak to `_generate_color_palette` to pass num_boxes and get a List of RGB values. Instead of List[Tensor]. The fix is simple and that can be passed to get RGB colors.


### Alternatives

Users can anyway use `color="white"` to get white boxes.

### Additional context

It's simple issue and good-first. We can probably wait for some new contributor :smiley:  to join.


Discussion:
I can take this if that's okay.
I'm glad that you want to take it. 
Just need a green signal from the maintainers @NicolasHug @datumbox 



Thanks for the proposal @oke-aditya and @gessha for volunteering.

That sounds good to me, we should just make sure that the same color is generated for all boxes with the same label.
Well I'm not sure how we would ensure that same color is generated for all boxes with same label.
My thoughts were to generate color palette equal to number of boxes. And then we simply pick ith color from the palette.

We don't keep consistency of randomly generating same color for same label in drawing segmentation masks too.


> Well I'm not sure how we would ensure that same color is generated for all boxes with same label.

We can generate num_labels colors and then assign one color per label. Obviously we don't need to do that if `labels` isn't specified

> We don't keep consistency of randomly generating same color for same label in drawing segmentation masks too.

True but `draw_segmentation_masks` doesn't accept a label anyway, as it probably doesn't need one.
Great, I think it should be feasible with some refactor. As of now we plot each bbox and then the labels separately.

@gessha feel free to send a PR!
Hey @gessha  are you working on this?
Or
@ABD-01 would you like to take this up?
Yes, I can if no one's working on it.

I guess the main change involved will be here
https://github.com/pytorch/vision/blob/a75dc89abbbb233469d1dbe2ee00ee012dd70522/torchvision/utils.py#L203-L204
Just to make the variable `colors` be a list returned from [`_generate_color_palette`](https://github.com/pytorch/vision/blob/a75dc89abbbb233469d1dbe2ee00ee012dd70522/torchvision/utils.py#L303), right?

Yes and use the colors the generated from the palette in draw_bounding_boxes. 
Create colors upto num_labels. 

Map each label to a color. 

Since labels is a list and generated color palette is also a list. Both have the same length. 
(As we generated colors equal to num_labels)

Use these colors to draw boxes. 


@ABD-01 I spent some time and thought a bit more.

Here is a brief pseudocode (highly untested, written on paper), this can be simplified / optimized as per needs :smiley: 

```

if colors is None:
   if labels is None:
       # We have no choice to create colors equal to number of boxes
       # gen_colors is  List[Tuple[int, int, int]]

        gen_colors = genereate_color_palette(len(img_boxes))
   else:
      # One way is to generate number of colors equal to number of unique labels.
      # Or alternatively, just generate excess colors and map each label just once.
      gen_colors = generate_color_palette(len(labels)) (or probably even len(img_boxes))
      
     # Since dictionary will have only unique keys.  
      label_color_map = {}
      for label, color in zip(label, gen_colors):
          label_color_map[label] = color

# Now we have color palette we can enumerate over boxes.
for i, bbox in enumerate(img_boxes):
    if colors is None:
        if labels is None:
            # No labels so just use the generated box colors
           color = gen_colors[i]
            # We have labels, so simply make use of label_color_map
        else:
           color = label_color_map[labels[i]]     
   else:
          # Main code continues.

```

Resolving patch:
diff --git a/torchvision/utils.py b/torchvision/utils.py
--- a/torchvision/utils.py
+++ b/torchvision/utils.py
@@ -176,6 +176,7 @@ def draw_bounding_boxes(
         colors (color or list of colors, optional): List containing the colors
             of the boxes or single color for all boxes. The color can be represented as
             PIL strings e.g. "red" or "#FF00FF", or as RGB tuples e.g. ``(240, 10, 157)``.
+            By default, random colors are generated for boxes.
         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
@@ -198,45 +199,50 @@ def draw_bounding_boxes(
     elif image.size(0) not in {1, 3}:
         raise ValueError("Only grayscale and RGB images are supported")
 
+    num_boxes = boxes.shape[0]
+
+    if labels is None:
+        labels: Union[List[str], List[None]] = [None] * num_boxes  # type: ignore[no-redef]
+    elif len(labels) != num_boxes:
+        raise ValueError(
+            f"Number of boxes ({num_boxes}) and labels ({len(labels)}) mismatch. Please specify labels for each box."
+        )
+
+    if colors is None:
+        colors = _generate_color_palette(num_boxes)
+    elif isinstance(colors, list):
+        if len(colors) < num_boxes:
+            raise ValueError(f"Number of colors ({len(colors)}) is less than number of boxes ({num_boxes}). ")
+    else:  # colors specifies a single color for all boxes
+        colors = [colors] * num_boxes
+
+    colors = [(ImageColor.getrgb(color) if isinstance(color, str) else color) for color in colors]
+
+    # Handle Grayscale images
     if image.size(0) == 1:
         image = torch.tile(image, (3, 1, 1))
 
     ndarr = image.permute(1, 2, 0).cpu().numpy()
     img_to_draw = Image.fromarray(ndarr)
-
     img_boxes = boxes.to(torch.int64).tolist()
 
     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):
-        if colors is None:
-            color = None
-        elif isinstance(colors, list):
-            color = colors[i]
-        else:
-            color = colors
-
+    for bbox, color, label in zip(img_boxes, colors, labels):  # type: ignore[arg-type]
         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,)
+            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:
+        if label is not None:
             margin = width + 1
-            draw.text((bbox[0] + margin, bbox[1] + margin), labels[i], fill=color, font=txt_font)
+            draw.text((bbox[0] + margin, bbox[1] + margin), label, fill=color, font=txt_font)
 
     return torch.from_numpy(np.array(img_to_draw)).permute(2, 0, 1).to(dtype=torch.uint8)
 
@@ -505,9 +511,9 @@ def _make_colorwheel() -> torch.Tensor:
     return colorwheel
 
 
-def _generate_color_palette(num_masks: int):
+def _generate_color_palette(num_objects: int):
     palette = torch.tensor([2 ** 25 - 1, 2 ** 15 - 1, 2 ** 21 - 1])
-    return [tuple((i * palette) % 255) for i in range(num_masks)]
+    return [tuple((i * palette) % 255) for i in range(num_objects)]
 
 
 def _log_api_usage_once(obj: Any) -> None:


Test patch:
diff --git a/test/test_utils.py b/test/test_utils.py
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -124,7 +124,7 @@ def test_draw_boxes_vanilla():
     img = torch.full((3, 100, 100), 0, dtype=torch.uint8)
     img_cp = img.clone()
     boxes_cp = boxes.clone()
-    result = utils.draw_bounding_boxes(img, boxes, fill=False, width=7)
+    result = utils.draw_bounding_boxes(img, boxes, fill=False, width=7, colors="white")
 
     path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "fakedata", "draw_boxes_vanilla.png")
     if not os.path.exists(path):
@@ -149,7 +149,11 @@ def test_draw_invalid_boxes():
     img_tp = ((1, 1, 1), (1, 2, 3))
     img_wrong1 = torch.full((3, 5, 5), 255, dtype=torch.float)
     img_wrong2 = torch.full((1, 3, 5, 5), 255, dtype=torch.uint8)
+    img_correct = torch.zeros((3, 10, 10), dtype=torch.uint8)
     boxes = torch.tensor([[0, 0, 20, 20], [0, 0, 0, 0], [10, 15, 30, 35], [23, 35, 93, 95]], dtype=torch.float)
+    labels_wrong = ["one", "two"]
+    colors_wrong = ["pink", "blue"]
+
     with pytest.raises(TypeError, match="Tensor expected"):
         utils.draw_bounding_boxes(img_tp, boxes)
     with pytest.raises(ValueError, match="Tensor uint8 expected"):
@@ -158,6 +162,10 @@ def test_draw_invalid_boxes():
         utils.draw_bounding_boxes(img_wrong2, boxes)
     with pytest.raises(ValueError, match="Only grayscale and RGB images are supported"):
         utils.draw_bounding_boxes(img_wrong2[0][:2], boxes)
+    with pytest.raises(ValueError, match="Number of boxes"):
+        utils.draw_bounding_boxes(img_correct, boxes, labels_wrong)
+    with pytest.raises(ValueError, match="Number of colors"):
+        utils.draw_bounding_boxes(img_correct, boxes, colors=colors_wrong)
 
 
 @pytest.mark.parametrize(