2

I have the below code where I'm using default augmentation during training of Detectron 2 but the problem is that there are very few augmentations that are useful to me.

  1. Here is something like what I to achieve with my own functions.

  2. This github issue link tries to solve.

Want to whether it is the right way to do it and also, how could I Look at the augmented image result if I want to see what's happening?

import detectron2.data.transforms as T
from detectron2.data import detection_utils as utils

def custom_mapper(dataset_dict):
    
    dataset_dict = copy.deepcopy(dataset_dict)
    image = utils.read_image(dataset_dict["file_name"], format="BGR")
    transform_list = [T.RandomBrightness(0.8, 1.2),
                      T.RandomContrast(0.8, 1.2),
                      T.RandomSaturation(0.8, 1.2),
                      ]
    image, transforms = T.apply_transform_gens(transform_list, image)
    dataset_dict["image"] = torch.as_tensor(image.transpose(2, 0, 1).astype("float32"))

    annos = [
        utils.transform_instance_annotations(obj, transforms, image.shape[:2])
        for obj in dataset_dict.pop("annotations")
        if obj.get("iscrowd", 0) == 0
    ]
    instances = utils.annotations_to_instances(annos, image.shape[:2])
    dataset_dict["instances"] = utils.filter_empty_instances(instances)
    return dataset_dict

But the problem is that

  1. I want to build my custom augmentation or use albumentations for the purpose.
  2. I don't want to use all of the augmentations every time and there's no probability in the above transformations that I have used. So a way around would be to use something like OneOf on individual or a group.
Deshwal
  • 3,436
  • 4
  • 35
  • 94
  • You might want to take a look at this similar question: [How to use detectron2's augmentation with datasets loaded using register_coco_instances](https://stackoverflow.com/questions/71774744/how-to-use-detectron2s-augmentation-with-datasets-loaded-using-register-coco-in) – Bilal Qandeel Sep 21 '22 at 21:05

2 Answers2

1

So after running through the code flow and documentation, I found out that Each Augmentation class is dependent on the Transform class which is inherited from the fvcore library

Also the same dependency is also defined vaguely in this highlighted documentation block

Then looking at all the aspects above and then going through this github PR merge fight thread, I sense that I'm on the right track so following PILColorTransform augmentation, I have made this code which works perfectly for custom augmentation

1. CUSTOM Augmentations

class GenericWrapperTransform(Transform):
    """
    Generic wrapper for any transform (for color transform only. You can give functionality to apply_coods, apply_segmentation too)
    """

    def __init__(self, custom_function:Callable):
        """
        Args:
            custom_function (Callable): operation to be applied to the image which takes in an ndarray and returns an ndarray.
        """
        if not callable(custom_function):
            raise ValueError("'custom_function' should be callable")
        
        super().__init__()
        self._set_attributes(locals())

    def apply_image(self, img):
        '''
        apply transformation to image array based on the `custom_function`
        '''
        return self.custom_function(img)

    def apply_coords(self, coords):
        '''
        Apply transformations to Bounding Box Coordinates. Currently is won't do anything but we can change this based on our use case
        '''
        return coords

    def inverse(self):
        return NoOpTransform()

    def apply_segmentation(self, segmentation):
        '''
        Apply transformations to segmentation. currently is won't do anything but we can change this based on our use case
        '''
        return segmentation


class CustomAug(Augmentation):
    """
    Given a probability and a custom function, return a GenericWrapperTransform object whose `apply_image`  will be called to perform augmentation
    """

    def __init__(self, custom_function, prob=1.0):
        """
        Args:
            custom_op: Operation to use. Must be a function takes an ndarray and returns an ndarray
            prob (float): probability of applying the function
        """
        super().__init__()
        self._init(locals())

    def get_transform(self, image):
        '''
        Based on probability, choose whether you want to apply the given function or not
        '''
        do = self._rand_range() < self.prob
        if do:
            return GenericWrapperTransform(self.custom_function)
        else:
            return NoOpTransform() # it returns a Transform which just returns the original Image array only


def white(image):
    return np.ones(image.shape, dtype = np.uint8)*255 # returns white Image

def black(image):
    return np.zeros(image.shape, dtype=np.uint8) # returns black image

def rand(image):
    return np.random.randint(0,256,image.shape, dtype = np.uint8) # returns random image

def default(image):
    return image # returns original image

2. Implementing OneOf like functions

I looked into the implementation of AugmentationList and built my own code to generate transformations randomly. Setting k=1 mimics OneOf like functionality.

class KRandomAugmentationList(Augmentation):
    """
    Select and Apply "K" augmentations in "RANDOM" order with "Every"  __call__ method invoke
    """
    def __init__(self, augs, k:int = -1):
        """
        Args:
            augs: list of [Augmentation or Transform]
            k: Number of augment to use from the given list in range [1,len_augs]. If None, use all. If it is -1, generate K randomly between [1,len_augs]
        """
        super().__init__()
        self.max_range = len(augs)
        self.k = k
        self.augs = augs # set augs to use as fixed if we have to use same augs everytime
    

    def _setup_augs(self, augs, k:int):
        '''
        Setup the argument list. Generates the list of argument to use from the given list
        args:
            augs: list of [Augmentation or Transform])
            k: Number of augment to use from the given list in range [1,len_augs]. If False, use all. If it is -1, generate K randomly between [1,len_augs]
        '''
        if k == -1: # Generate a random number
            k = np.random.randint(1,len(augs)+1)
        
        elif k is None: # use all
            k = self.max_range

        temp = np.random.choice(augs,k,replace=False) # get k augments randomly
        return [_transform_to_aug(x) for x in temp]

    
    def __call__(self, aug_input) -> Transform:
        tfms = []

        for x in self._setup_augs(self.augs, self.k): # generate auguments to use randomly on the fly
            print(x)
            tfm = x(aug_input)
            tfms.append(tfm)
        return TransformList(tfms)

    def __repr__(self):
        msgs = [str(x) for x in self.augs]
        return "AugmentationList[{}]".format(", ".join(msgs))

    __str__ = __repr__

3. Putting Everything together

from detectron2.data import transforms as T
import numpy as np
from PIL import Image

augs = KRandomAugmentationList(
        [
        # my custom augs
        CustomAug(white), 
        CustomAug(black),
        CustomAug(default),
        CustomAug(rand),
        
        # augs from Detectron
        T.RandomBrightness(0.4, 1.6),
        T.RandomSaturation(0.4, 1.6),
        T.RandomContrast(0.4,1.6),
        T.RandomCrop("absolute", (640, 640)),
        T.RandomFlip(prob=0.5),
        ],
        k = -1)



# Calling the below block multiple times will give you different combinations
# of Augmentations everytime depending on the value of `k`

image = np.array(Image.open("my_image.png")) # RGB image array
input_ = T.AugInput(image, boxes=None, sem_seg = None) # boxes and segments are optional
transform = augs(input_)  # Apply the augmentation
image_transformed = input_.image  # augmented image

Image.fromarray(image_transformed) # show RGB image


Deshwal
  • 3,436
  • 4
  • 35
  • 94
-1

You can easily do both things with albumentations.

Here is an example:

import albumentations as A

def create_transform():
    return A.Compose(
        [
            A.OneOf([
                A.RandomBrightness(p=1),
                A.RandomContrast(limit=.2, p=1)
                ],
                p=.8),
            A.GaussNoise(p=.8),
        ],
    )

def transform_image(image):
    
    transform = create_transform()
    
    transformed = transform(image=image)
    
    transformed_image = transformed["image"]
    
    return transformed_image 

For every albumentations operation there is a probability parameter p=1 and for randomly chose a operation between two or more you can use A.OneOf([],p=.8). In my example both A.OneOf([],p=.8) and A.GaussNoise(p=.8) 80% chance of applying on the image.

Testing with a random image

import numpy as np

image = np.random.randint(255, size=(24,24)).astype("float32")

# array([[104.,  29., 230.,  68., 134.],
#        [ 43., 162.,  73., 246., 111.],
#        [ 71., 188., 206.,  85., 233.],
#        [105., 178., 190.,  54., 231.],
#        [  0., 116., 169., 244., 178.]], dtype=float32)

transform_image(a)

# array([[1.        , 1.        , 1.        , 1.        , 0.11930324],
#        [1.        , 1.        , 0.        , 0.        , 1.        ],
#        [0.        , 0.        , 1.        , 0.5334698 , 0.        ],
#        [1.        , 0.        , 0.        , 1.        , 1.        ],
#        [0.        , 0.        , 1.        , 0.        , 1.        ]],
#       dtype=float32)
rafathasan
  • 524
  • 3
  • 15
  • Yes I know but but I want to use it in `Detectron2`. Which accepts `Augmentation` and `Transform` classes only. It also registers the functions to use them later for different tasks like caching, pre fetching etc. – Deshwal Oct 21 '22 at 08:57