Repository: trusted-ai/adversarial-robustness-toolbox
Issue: Trigger Placement Bugs for Image Poisoning Perturbations

**Describe the bug**
The `insert_image` function in `art.attacks.poisoning.perturbations` has two bugs involving the placement of the trigger image.

1. Due to Pillow swapping the height and width, a non-square trigger is the wrong dimensions when placed onto the image. This was partially addressed by PR #2046 which fixed the incorrect dimensions for the input image, but did not make the same change for the trigger. The fix is is to apply the same where the height and width are swapped.

2. When the trigger height or width is the same size as the image height or width, the `insert_image` function will error due to the `np.random.randint` function not accepting 0 as the upper bound. This numpy function will sample from the lower bound inclusively and upper bound exclusively. This is an off-by-one error and can be easily fixed by adding one to the difference between the image height/width and trigger height/width.

**To Reproduce**
1. The following code snippet will reproduce the first bug:

    ```python
    import numpy as np
    import matplotlib.pyplot as plt
    from art.attacks.poisoning.perturbations import insert_image
    
    image = np.ones((32, 32, 3))
    image_poisoned = insert_image(
        image,
        backdoor_path='../utils/data/backdoors/htbd.png',
        size=(5, 10),
        random=False,
        x_shift=0,
        y_shift=0,
        mode='RGB'
    )
    plt.imshow(image_poisoned)
    ```
    
    From this, the trigger will be inserted as a `10 x 5` rather than the specified `5 x 10` due to Pillow swapping the height and width order.

2. The following code snippet will reproduce the second bug:

    ```python
    import numpy as np
    import matplotlib.pyplot as plt
    from art.attacks.poisoning.perturbations import insert_image
    
    image = np.ones((32, 32, 3))
    image_poisoned = insert_image(
        image,
        backdoor_path='../utils/data/backdoors/htbd.png',
        size=(32, 32),
        random=True,
        mode='RGB'
    )
    plt.imshow(image_poisoned)
    ```
    
    This will raise an exception due to the `np.random.randint` function not accepting a value of 0 as the trigger and image size are the same. 

**Expected behavior**
The `insert_image` function should produce the correct behavior and not error when valid inputs are provided.

**Screenshots**
N/A

**System information (please complete the following information):**
 - OS
 - Python version
 - ART version or commit number
 - TensorFlow / Keras / PyTorch / MXNet version


Changed files:
art/attacks/poisoning/perturbations/image_perturbations.py

diff --git a/art/attacks/poisoning/perturbations/image_perturbations.py b/art/attacks/poisoning/perturbations/image_perturbations.py
index bfb861a8e..9355f054a 100644
--- a/art/attacks/poisoning/perturbations/image_perturbations.py
+++ b/art/attacks/poisoning/perturbations/image_perturbations.py
@@ -127,7 +127,7 @@ def insert_image(
     original_dtype = x.dtype
     data = np.copy(x)
     if channels_first:
-        data = data.transpose([1, 2, 0])
+        data = np.transpose(data, (1, 2, 0))
 
     height, width, num_channels = data.shape
 
@@ -136,15 +136,15 @@ def insert_image(
     backdoored_img = Image.new("RGBA", (width, height), 0)  # height and width are swapped for PIL
 
     if no_color:
-        backdoored_input = Image.fromarray((data * 255).astype("uint8").squeeze(axis=2), mode=mode)
+        backdoored_input = Image.fromarray((data * 255).astype(np.uint8).squeeze(axis=2), mode=mode)
     else:
-        backdoored_input = Image.fromarray((data * 255).astype("uint8"), mode=mode)
+        backdoored_input = Image.fromarray((data * 255).astype(np.uint8), mode=mode)
 
     orig_img.paste(backdoored_input)
 
     trigger = Image.open(backdoor_path).convert("RGBA")
-    if size:
-        trigger = trigger.resize(size)
+    if size is not None:
+        trigger = trigger.resize((size[1], size[0]))  # height and width are swapped for PIL
 
     backdoor_width, backdoor_height = trigger.size  # height and width are swapped for PIL
 
@@ -152,21 +152,20 @@ def insert_image(
         raise ValueError("Backdoor does not fit inside original image")
 
     if random:
-        x_shift = np.random.randint(width - backdoor_width)
-        y_shift = np.random.randint(height - backdoor_height)
+        x_shift = np.random.randint(width - backdoor_width + 1)
+        y_shift = np.random.randint(height - backdoor_height + 1)
 
     backdoored_img.paste(trigger, (x_shift, y_shift), mask=trigger)
     composite = Image.alpha_composite(orig_img, backdoored_img)
     backdoored_img = Image.blend(orig_img, composite, blend)
-
     backdoored_img = backdoored_img.convert(mode)
 
-    res = np.array(backdoored_img) / 255.0
+    res = np.asarray(backdoored_img) / 255.0
 
     if no_color:
         res = np.expand_dims(res, 2)
 
     if channels_first:
-        res = res.transpose([2, 0, 1])
+        res = np.transpose(res, (2, 0, 1))
 
     return res.astype(original_dtype)