diff --git a/Augmentor/Operations.py b/Augmentor/Operations.py index 26b94ba..09c9253 100644 --- a/Augmentor/Operations.py +++ b/Augmentor/Operations.py @@ -20,8 +20,7 @@ For detailed information on extending Augmentor, see :ref:`extendingaugmentor`. """ -from __future__ import (absolute_import, division, - print_function, unicode_literals) +from __future__ import absolute_import, division, print_function, unicode_literals from builtins import * from PIL import Image, ImageOps, ImageEnhance @@ -29,6 +28,7 @@ from math import floor, ceil import numpy as np + # from skimage import img_as_ubyte # from skimage import transform @@ -51,7 +51,8 @@ class Operation(object): the section on extending Augmentor with custom operations at :ref:`extendingaugmentor`. """ - def __init__(self, probability): + + def __init__(self, probability, skip_gt_image): """ All operations must at least have a :attr:`probability` which is initialised when creating the operation's object. @@ -61,6 +62,7 @@ def __init__(self, probability): :type probability: Float """ self.probability = probability + self.skip_gt_image = skip_gt_image # check if need to skip operation once def __str__(self): """ @@ -93,7 +95,8 @@ class HistogramEqualisation(Operation): The class :class:`HistogramEqualisation` is used to perform histogram equalisation on images passed to its :func:`perform_operation` function. """ - def __init__(self, probability): + + def __init__(self, probability, skip_gt_image): """ As there are no further user definable parameters, the class is instantiated using only the :attr:`probability` argument. @@ -102,7 +105,7 @@ def __init__(self, probability): performed when it is invoked in the pipeline. :type probability: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) def perform_operation(self, images): """ @@ -140,7 +143,8 @@ class Greyscale(Operation): .. seealso:: The :class:`BlackAndWhite` class. """ - def __init__(self, probability): + + def __init__(self, probability, skip_gt_image): """ As there are no further user definable parameters, the class is instantiated using only the :attr:`probability` argument. @@ -149,7 +153,7 @@ def __init__(self, probability): performed when it is invoked in the pipeline. :type probability: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) def perform_operation(self, images): """ @@ -178,7 +182,8 @@ class Invert(Operation): This class is used to negate images. That is to reverse the pixel values for any image processed by it. """ - def __init__(self, probability): + + def __init__(self, probability, skip_gt_image): """ As there are no further user definable parameters, the class is instantiated using only the :attr:`probability` argument. @@ -187,7 +192,7 @@ def __init__(self, probability): performed when it is invoked in the pipeline. :type probability: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) def perform_operation(self, images): """ @@ -220,7 +225,8 @@ class BlackAndWhite(Operation): .. seealso:: The :class:`Greyscale` class. """ - def __init__(self, probability, threshold): + + def __init__(self, probability, skip_gt_image, threshold): """ As well as the required :attr:`probability` parameter, a :attr:`threshold` can also be defined to define the cutoff point where @@ -235,7 +241,7 @@ def __init__(self, probability, threshold): :type probability: Float :type threshold: Integer """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.threshold = threshold def perform_operation(self, images): @@ -270,7 +276,8 @@ class RandomBrightness(Operation): """ This class is used to random change image brightness. """ - def __init__(self, probability, min_factor, max_factor): + + def __init__(self, probability, skip_gt_image, min_factor, max_factor): """ required :attr:`probability` parameter @@ -289,7 +296,7 @@ def __init__(self, probability, min_factor, max_factor): :type max_factor: Float :type max_factor: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.min_factor = min_factor self.max_factor = max_factor @@ -321,7 +328,8 @@ class RandomColor(Operation): """ This class is used to random change saturation of an image. """ - def __init__(self, probability, min_factor, max_factor): + + def __init__(self, probability, skip_gt_image, min_factor, max_factor): """ required :attr:`probability` parameter @@ -340,7 +348,7 @@ def __init__(self, probability, min_factor, max_factor): :type max_factor: Float :type max_factor: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.min_factor = min_factor self.max_factor = max_factor @@ -372,7 +380,8 @@ class RandomContrast(Operation): """ This class is used to random change contrast of an image. """ - def __init__(self, probability, min_factor,max_factor): + + def __init__(self, probability, skip_gt_image, min_factor, max_factor): """ required :attr:`probability` parameter @@ -391,7 +400,7 @@ def __init__(self, probability, min_factor,max_factor): :type max_factor: Float :type max_factor: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.min_factor = min_factor self.max_factor = max_factor @@ -424,7 +433,8 @@ class Skew(Operation): This class is used to perform perspective skewing on images. It allows for skewing from a total of 12 different perspectives. """ - def __init__(self, probability, skew_type, magnitude): + + def __init__(self, probability, skip_gt_image, skew_type, magnitude): """ As well as the required :attr:`probability` parameter, the type of skew that is performed is controlled using a :attr:`skew_type` and a @@ -464,7 +474,7 @@ def __init__(self, probability, skew_type, magnitude): :type skew_type: String :type magnitude: Integer """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.skew_type = skew_type self.magnitude = magnitude @@ -531,28 +541,36 @@ def perform_operation(self, images): if skew_direction == 0: # Left Tilt - new_plane = [(y1, x1 - skew_amount), # Top Left - (y2, x1), # Top Right - (y2, x2), # Bottom Right - (y1, x2 + skew_amount)] # Bottom Left + new_plane = [ + (y1, x1 - skew_amount), # Top Left + (y2, x1), # Top Right + (y2, x2), # Bottom Right + (y1, x2 + skew_amount), + ] # Bottom Left elif skew_direction == 1: # Right Tilt - new_plane = [(y1, x1), # Top Left - (y2, x1 - skew_amount), # Top Right - (y2, x2 + skew_amount), # Bottom Right - (y1, x2)] # Bottom Left + new_plane = [ + (y1, x1), # Top Left + (y2, x1 - skew_amount), # Top Right + (y2, x2 + skew_amount), # Bottom Right + (y1, x2), + ] # Bottom Left elif skew_direction == 2: # Forward Tilt - new_plane = [(y1 - skew_amount, x1), # Top Left - (y2 + skew_amount, x1), # Top Right - (y2, x2), # Bottom Right - (y1, x2)] # Bottom Left + new_plane = [ + (y1 - skew_amount, x1), # Top Left + (y2 + skew_amount, x1), # Top Right + (y2, x2), # Bottom Right + (y1, x2), + ] # Bottom Left elif skew_direction == 3: # Backward Tilt - new_plane = [(y1, x1), # Top Left - (y2, x1), # Top Right - (y2 + skew_amount, x2), # Bottom Right - (y1 - skew_amount, x2)] # Bottom Left + new_plane = [ + (y1, x1), # Top Left + (y2, x1), # Top Right + (y2 + skew_amount, x2), # Bottom Right + (y1 - skew_amount, x2), + ] # Bottom Left if skew == "CORNER": @@ -589,12 +607,29 @@ def perform_operation(self, images): # It may make sense to keep this, if we ensure the skew_amount below is randomised # and cannot be manually set by the user. corners = dict() - corners["top_left"] = (y1 - random.randint(1, skew_amount), x1 - random.randint(1, skew_amount)) - corners["top_right"] = (y2 + random.randint(1, skew_amount), x1 - random.randint(1, skew_amount)) - corners["bottom_right"] = (y2 + random.randint(1, skew_amount), x2 + random.randint(1, skew_amount)) - corners["bottom_left"] = (y1 - random.randint(1, skew_amount), x2 + random.randint(1, skew_amount)) - - new_plane = [corners["top_left"], corners["top_right"], corners["bottom_right"], corners["bottom_left"]] + corners["top_left"] = ( + y1 - random.randint(1, skew_amount), + x1 - random.randint(1, skew_amount), + ) + corners["top_right"] = ( + y2 + random.randint(1, skew_amount), + x1 - random.randint(1, skew_amount), + ) + corners["bottom_right"] = ( + y2 + random.randint(1, skew_amount), + x2 + random.randint(1, skew_amount), + ) + corners["bottom_left"] = ( + y1 - random.randint(1, skew_amount), + x2 + random.randint(1, skew_amount), + ) + + new_plane = [ + corners["top_left"], + corners["top_right"], + corners["bottom_right"], + corners["bottom_left"], + ] # To calculate the coefficients required by PIL for the perspective skew, # see the following Stack Overflow discussion: https://goo.gl/sSgJdj @@ -608,13 +643,17 @@ def perform_operation(self, images): B = np.array(original_plane).reshape(8) perspective_skew_coefficients_matrix = np.dot(np.linalg.pinv(A), B) - perspective_skew_coefficients_matrix = np.array(perspective_skew_coefficients_matrix).reshape(8) + perspective_skew_coefficients_matrix = np.array( + perspective_skew_coefficients_matrix + ).reshape(8) def do(image): - return image.transform(image.size, - Image.PERSPECTIVE, - perspective_skew_coefficients_matrix, - resample=Image.BICUBIC) + return image.transform( + image.size, + Image.PERSPECTIVE, + perspective_skew_coefficients_matrix, + resample=Image.BICUBIC, + ) augmented_images = [] @@ -634,12 +673,20 @@ class RotateStandard(Operation): .. seealso:: For 90 degree rotations, see the :class:`Rotate` class. """ - def __init__(self, probability, max_left_rotation, max_right_rotation, expand=False, fillcolor=None): + def __init__( + self, + probability, + skip_gt_image, + max_left_rotation, + max_right_rotation, + expand=False, + fillcolor=None, + ): """ Documentation to appear. """ - Operation.__init__(self, probability) - self.max_left_rotation = -abs(max_left_rotation) # Ensure always negative + Operation.__init__(self, probability, skip_gt_image) + self.max_left_rotation = -abs(max_left_rotation) # Ensure always negative self.max_right_rotation = abs(max_right_rotation) # Ensure always positive self.expand = expand self.fillcolor = fillcolor @@ -666,7 +713,9 @@ def perform_operation(self, images): rotation = random_right def do(image): - return image.rotate(rotation, expand=self.expand, resample=Image.BICUBIC, fillcolor=self.fillcolor) + return image.rotate( + rotation, expand=self.expand, resample=Image.BICUBIC, fillcolor=self.fillcolor + ) augmented_images = [] @@ -683,7 +732,7 @@ class Rotate(Operation): class. """ - def __init__(self, probability, rotation): + def __init__(self, probability, skip_gt_image, rotation): """ As well as the required :attr:`probability` parameter, the :attr:`rotation` parameter controls the rotation to perform, @@ -702,7 +751,7 @@ def __init__(self, probability, rotation): .. seealso:: For arbitrary rotations, see the :class:`RotateRange` class. """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.rotation = rotation def __str__(self): @@ -758,7 +807,8 @@ class RotateRange(Operation): The :ref:`rotating` section describes this in detail and has example images to demonstrate this. """ - def __init__(self, probability, max_left_rotation, max_right_rotation): + + def __init__(self, probability, skip_gt_image, max_left_rotation, max_right_rotation): """ As well as the required :attr:`probability` parameter, the :attr:`max_left_rotation` parameter controls the maximum number of @@ -776,8 +826,8 @@ def __init__(self, probability, max_left_rotation, max_right_rotation): :type max_left_rotation: Integer :type max_right_rotation: Integer """ - Operation.__init__(self, probability) - self.max_left_rotation = -abs(max_left_rotation) # Ensure always negative + Operation.__init__(self, probability, skip_gt_image) + self.max_left_rotation = -abs(max_left_rotation) # Ensure always negative self.max_right_rotation = abs(max_right_rotation) # Ensure always positive def perform_operation(self, images): @@ -832,8 +882,11 @@ def do(image): angle_b_sin = math.sin(angle_b_rad) # Find the maximum area of the rectangle that could be cropped - E = (math.sin(angle_a_rad)) / (math.sin(angle_b_rad)) * \ - (Y - X * (math.sin(angle_a_rad) / math.sin(angle_b_rad))) + E = ( + (math.sin(angle_a_rad)) + / (math.sin(angle_b_rad)) + * (Y - X * (math.sin(angle_a_rad) / math.sin(angle_b_rad))) + ) E = E / 1 - (math.sin(angle_a_rad) ** 2 / math.sin(angle_b_rad) ** 2) B = X - E A = (math.sin(angle_a_rad) / math.sin(angle_b_rad)) * B @@ -857,7 +910,8 @@ class Resize(Operation): """ This class is used to resize images by absolute values passed as parameters. """ - def __init__(self, probability, width, height, resample_filter): + + def __init__(self, probability, skip_gt_image, width, height, resample_filter): """ Accepts the required probability parameter as well as parameters to control the size of the transformed image. @@ -874,7 +928,7 @@ def __init__(self, probability, width, height, resample_filter): :type height: Integer :type resample_filter: String """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.width = width self.height = height self.resample_filter = resample_filter @@ -909,7 +963,8 @@ class Flip(Operation): The class allows an image to be mirrored along either its x axis or its y axis, or randomly. """ - def __init__(self, probability, top_bottom_left_right): + + def __init__(self, probability, skip_gt_image, top_bottom_left_right): """ The direction of the flip, or whether it should be randomised, is controlled using the :attr:`top_bottom_left_right` parameter. @@ -925,7 +980,7 @@ def __init__(self, probability, top_bottom_left_right): - ``RANDOM`` defines that the image is mirrored randomly along either the x or y axis. """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.top_bottom_left_right = top_bottom_left_right def perform_operation(self, images): @@ -964,7 +1019,8 @@ class Crop(Operation): """ This class is used to crop images by absolute values passed as parameters. """ - def __init__(self, probability, width, height, centre): + + def __init__(self, probability, skip_gt_image, width, height, centre): """ As well as the always required :attr:`probability` parameter, the constructor requires a :attr:`width` to control the width of @@ -985,7 +1041,7 @@ def __init__(self, probability, width, height, centre): :type height: Integer :type centre: Boolean """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image, skip_gt_image) self.width = width self.height = height self.centre = centre @@ -1001,7 +1057,9 @@ def perform_operation(self, images): PIL.Image. """ - w, h = images[0].size # All images must be the same size, so we can just check the first image in the list + w, h = images[ + 0 + ].size # All images must be the same size, so we can just check the first image in the list left_shift = random.randint(0, int((w - self.width))) down_shift = random.randint(0, int((h - self.height))) @@ -1013,9 +1071,18 @@ def do(image): return image if self.centre: - return image.crop(((w/2)-(self.width/2), (h/2)-(self.height/2), (w/2)+(self.width/2), (h/2)+(self.height/2))) + return image.crop( + ( + (w / 2) - (self.width / 2), + (h / 2) - (self.height / 2), + (w / 2) + (self.width / 2), + (h / 2) + (self.height / 2), + ) + ) else: - return image.crop((left_shift, down_shift, self.width + left_shift, self.height + down_shift)) + return image.crop( + (left_shift, down_shift, self.width + left_shift, self.height + down_shift) + ) augmented_images = [] @@ -1029,7 +1096,10 @@ class CropPercentage(Operation): """ This class is used to crop images by a percentage of their area. """ - def __init__(self, probability, percentage_area, centre, randomise_percentage_area): + + def __init__( + self, probability, skip_gt_image, percentage_area, centre, randomise_percentage_area + ): """ As well as the always required :attr:`probability` parameter, the constructor requires a :attr:`percentage_area` to control the area @@ -1048,7 +1118,7 @@ def __init__(self, probability, percentage_area, centre, randomise_percentage_ar :type percentage_area: Float :type centre: Boolean """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.percentage_area = percentage_area self.centre = centre self.randomise_percentage_area = randomise_percentage_area @@ -1072,7 +1142,9 @@ def perform_operation(self, images): # The images must be of identical size, which is checked by Pipeline.ground_truth(). w, h = images[0].size - w_new = int(floor(w * r_percentage_area)) # TODO: Floor might return 0, so we need to check this. + w_new = int( + floor(w * r_percentage_area) + ) # TODO: Floor might return 0, so we need to check this. h_new = int(floor(h * r_percentage_area)) left_shift = random.randint(0, int((w - w_new))) @@ -1080,7 +1152,14 @@ def perform_operation(self, images): def do(image): if self.centre: - return image.crop(((w/2)-(w_new/2), (h/2)-(h_new/2), (w/2)+(w_new/2), (h/2)+(h_new/2))) + return image.crop( + ( + (w / 2) - (w_new / 2), + (h / 2) - (h_new / 2), + (w / 2) + (w_new / 2), + (h / 2) + (h_new / 2), + ) + ) else: return image.crop((left_shift, down_shift, w_new + left_shift, h_new + down_shift)) @@ -1098,7 +1177,8 @@ class CropRandom(Operation): of the user-facing functions in the :class:`~Augmentor.Pipeline.Pipeline` class. """ - def __init__(self, probability, percentage_area): + + def __init__(self, probability, skip_gt_image, percentage_area): """ :param probability: Controls the probability that the operation is performed when it is invoked in the pipeline. @@ -1106,7 +1186,7 @@ def __init__(self, probability, percentage_area): to crop. A value of 0.5 would crop an area that is 50% of the area of the original image's size. """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.percentage_area = percentage_area def perform_operation(self, images): @@ -1124,11 +1204,20 @@ def perform_operation(self, images): w_new = int(floor(w * self.percentage_area)) h_new = int(floor(h * self.percentage_area)) - random_left_shift = random.randint(0, int((w - w_new))) # Note: randint() is from uniform distribution. + random_left_shift = random.randint( + 0, int((w - w_new)) + ) # Note: randint() is from uniform distribution. random_down_shift = random.randint(0, int((h - h_new))) def do(image): - return image.crop((random_left_shift, random_down_shift, w_new + random_left_shift, h_new + random_down_shift)) + return image.crop( + ( + random_left_shift, + random_down_shift, + w_new + random_left_shift, + h_new + random_down_shift, + ) + ) augmented_images = [] @@ -1154,7 +1243,8 @@ class Shear(Operation): For sample code with image examples see :ref:`shearing`. """ - def __init__(self, probability, max_shear_left, max_shear_right): + + def __init__(self, probability, skip_gt_image, max_shear_left, max_shear_right): """ The shearing is randomised in magnitude, from 0 to the :attr:`max_shear_left` or 0 to :attr:`max_shear_right` where the @@ -1170,7 +1260,7 @@ def __init__(self, probability, max_shear_left, max_shear_right): :type max_shear_left: Integer :type max_shear_right: Integer """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.max_shear_left = max_shear_left self.max_shear_right = max_shear_right @@ -1207,8 +1297,11 @@ def perform_operation(self, images): # max_shear_left = 20 # max_shear_right = 20 - angle_to_shear = int(random.uniform((abs(self.max_shear_left)*-1) - 1, self.max_shear_right + 1)) - if angle_to_shear != -1: angle_to_shear += 1 + angle_to_shear = int( + random.uniform((abs(self.max_shear_left) * -1) - 1, self.max_shear_right + 1) + ) + if angle_to_shear != -1: + angle_to_shear += 1 # Alternative method # Calculate our offset when cropping @@ -1258,13 +1351,14 @@ def do(image): phi = abs(phi) * -1 # Note: PIL expects the inverse scale, so 1/scale_factor for example. - transform_matrix = (1, phi, -matrix_offset, - 0, 1, 0) + transform_matrix = (1, phi, -matrix_offset, 0, 1, 0) - image = image.transform((int(round(width + shift_in_pixels)), height), - Image.AFFINE, - transform_matrix, - Image.BICUBIC) + image = image.transform( + (int(round(width + shift_in_pixels)), height), + Image.AFFINE, + transform_matrix, + Image.BICUBIC, + ) image = image.crop((abs(shift_in_pixels), 0, width, height)) @@ -1279,13 +1373,14 @@ def do(image): matrix_offset = 0 phi = abs(phi) * -1 - transform_matrix = (1, 0, 0, - phi, 1, -matrix_offset) + transform_matrix = (1, 0, 0, phi, 1, -matrix_offset) - image = image.transform((width, int(round(height + shift_in_pixels))), - Image.AFFINE, - transform_matrix, - Image.BICUBIC) + image = image.transform( + (width, int(round(height + shift_in_pixels))), + Image.AFFINE, + transform_matrix, + Image.BICUBIC, + ) image = image.crop((0, abs(shift_in_pixels), width, height)) @@ -1310,7 +1405,8 @@ class Scale(Operation): This function will return images that are **larger** than the input images. """ - def __init__(self, probability, scale_factor): + + def __init__(self, probability, skip_gt_image, scale_factor): """ As the aspect ratio is always kept constant, only a :attr:`scale_factor` is required for scaling the image. @@ -1322,7 +1418,7 @@ def __init__(self, probability, scale_factor): :type probability: Float :type scale_factor: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.scale_factor = scale_factor def perform_operation(self, images): @@ -1356,7 +1452,8 @@ class Distort(Operation): """ This class performs randomised, elastic distortions on images. """ - def __init__(self, probability, grid_width, grid_height, magnitude): + + def __init__(self, probability, skip_gt_image, grid_width, grid_height, magnitude): """ As well as the probability, the granularity of the distortions produced by this class can be controlled using the width and @@ -1379,7 +1476,7 @@ def __init__(self, probability, grid_width, grid_height, magnitude): :type grid_height: Integer :type magnitude: Integer """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.grid_width = grid_width self.grid_height = grid_height self.magnitude = abs(magnitude) @@ -1412,26 +1509,44 @@ def perform_operation(self, images): for vertical_tile in range(vertical_tiles): for horizontal_tile in range(horizontal_tiles): - if vertical_tile == (vertical_tiles - 1) and horizontal_tile == (horizontal_tiles - 1): - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_last_square + (horizontal_tile * width_of_square), - height_of_last_square + (height_of_square * vertical_tile)]) + if vertical_tile == (vertical_tiles - 1) and horizontal_tile == ( + horizontal_tiles - 1 + ): + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_last_square + (horizontal_tile * width_of_square), + height_of_last_square + (height_of_square * vertical_tile), + ] + ) elif vertical_tile == (vertical_tiles - 1): - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_square + (horizontal_tile * width_of_square), - height_of_last_square + (height_of_square * vertical_tile)]) + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_square + (horizontal_tile * width_of_square), + height_of_last_square + (height_of_square * vertical_tile), + ] + ) elif horizontal_tile == (horizontal_tiles - 1): - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_last_square + (horizontal_tile * width_of_square), - height_of_square + (height_of_square * vertical_tile)]) + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_last_square + (horizontal_tile * width_of_square), + height_of_square + (height_of_square * vertical_tile), + ] + ) else: - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_square + (horizontal_tile * width_of_square), - height_of_square + (height_of_square * vertical_tile)]) + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_square + (horizontal_tile * width_of_square), + height_of_square + (height_of_square * vertical_tile), + ] + ) # For loop that generates polygons could be rewritten, but maybe harder to read? # polygons = [x1,y1, x1,y2, x2,y2, x2,y1 for x1,y1, x2,y2 in dimensions] @@ -1439,9 +1554,12 @@ def perform_operation(self, images): # last_column = [(horizontal_tiles - 1) + horizontal_tiles * i for i in range(vertical_tiles)] last_column = [] for i in range(vertical_tiles): - last_column.append((horizontal_tiles-1)+horizontal_tiles*i) + last_column.append((horizontal_tiles - 1) + horizontal_tiles * i) - last_row = range((horizontal_tiles * vertical_tiles) - horizontal_tiles, horizontal_tiles * vertical_tiles) + last_row = range( + (horizontal_tiles * vertical_tiles) - horizontal_tiles, + horizontal_tiles * vertical_tiles, + ) polygons = [] for x1, y1, x2, y2 in dimensions: @@ -1457,28 +1575,16 @@ def perform_operation(self, images): dy = random.randint(-self.magnitude, self.magnitude) x1, y1, x2, y2, x3, y3, x4, y4 = polygons[a] - polygons[a] = [x1, y1, - x2, y2, - x3 + dx, y3 + dy, - x4, y4] + polygons[a] = [x1, y1, x2, y2, x3 + dx, y3 + dy, x4, y4] x1, y1, x2, y2, x3, y3, x4, y4 = polygons[b] - polygons[b] = [x1, y1, - x2 + dx, y2 + dy, - x3, y3, - x4, y4] + polygons[b] = [x1, y1, x2 + dx, y2 + dy, x3, y3, x4, y4] x1, y1, x2, y2, x3, y3, x4, y4 = polygons[c] - polygons[c] = [x1, y1, - x2, y2, - x3, y3, - x4 + dx, y4 + dy] + polygons[c] = [x1, y1, x2, y2, x3, y3, x4 + dx, y4 + dy] x1, y1, x2, y2, x3, y3, x4, y4 = polygons[d] - polygons[d] = [x1 + dx, y1 + dy, - x2, y2, - x3, y3, - x4, y4] + polygons[d] = [x1 + dx, y1 + dy, x2, y2, x3, y3, x4, y4] generated_mesh = [] for i in range(len(dimensions)): @@ -1500,7 +1606,21 @@ class GaussianDistortion(Operation): """ This class performs randomised, elastic gaussian distortions on images. """ - def __init__(self, probability, grid_width, grid_height, magnitude, corner, method, mex, mey, sdx, sdy): + + def __init__( + self, + probability, + skip_gt_image, + grid_width, + grid_height, + magnitude, + corner, + method, + mex, + mey, + sdx, + sdy, + ): """ As well as the probability, the granularity of the distortions produced by this class can be controlled using the width and @@ -1549,7 +1669,7 @@ def __init__(self, probability, grid_width, grid_height, magnitude, corner, meth e^{- \Big( \\frac{(x-\\text{mex})^2}{\\text{sdx}} + \\frac{(y-\\text{mey})^2}{\\text{sdy}} \Big) } """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.grid_width = grid_width self.grid_height = grid_height self.magnitude = abs(magnitude) @@ -1586,32 +1706,53 @@ def perform_operation(self, images): for vertical_tile in range(vertical_tiles): for horizontal_tile in range(horizontal_tiles): - if vertical_tile == (vertical_tiles - 1) and horizontal_tile == (horizontal_tiles - 1): - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_last_square + (horizontal_tile * width_of_square), - height_of_last_square + (height_of_square * vertical_tile)]) + if vertical_tile == (vertical_tiles - 1) and horizontal_tile == ( + horizontal_tiles - 1 + ): + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_last_square + (horizontal_tile * width_of_square), + height_of_last_square + (height_of_square * vertical_tile), + ] + ) elif vertical_tile == (vertical_tiles - 1): - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_square + (horizontal_tile * width_of_square), - height_of_last_square + (height_of_square * vertical_tile)]) + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_square + (horizontal_tile * width_of_square), + height_of_last_square + (height_of_square * vertical_tile), + ] + ) elif horizontal_tile == (horizontal_tiles - 1): - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_last_square + (horizontal_tile * width_of_square), - height_of_square + (height_of_square * vertical_tile)]) + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_last_square + (horizontal_tile * width_of_square), + height_of_square + (height_of_square * vertical_tile), + ] + ) else: - dimensions.append([horizontal_tile * width_of_square, - vertical_tile * height_of_square, - width_of_square + (horizontal_tile * width_of_square), - height_of_square + (height_of_square * vertical_tile)]) + dimensions.append( + [ + horizontal_tile * width_of_square, + vertical_tile * height_of_square, + width_of_square + (horizontal_tile * width_of_square), + height_of_square + (height_of_square * vertical_tile), + ] + ) last_column = [] for i in range(vertical_tiles): - last_column.append((horizontal_tiles-1)+horizontal_tiles*i) + last_column.append((horizontal_tiles - 1) + horizontal_tiles * i) - last_row = range((horizontal_tiles * vertical_tiles) - horizontal_tiles, horizontal_tiles * vertical_tiles) + last_row = range( + (horizontal_tiles * vertical_tiles) - horizontal_tiles, + horizontal_tiles * vertical_tiles, + ) polygons = [] for x1, y1, x2, y2 in dimensions: @@ -1623,8 +1764,12 @@ def perform_operation(self, images): polygon_indices.append([i, i + 1, i + horizontal_tiles, i + 1 + horizontal_tiles]) def sigmoidf(x, y, sdx=0.05, sdy=0.05, mex=0.5, mey=0.5, const=1): - sigmoid = lambda x1, y1: (const * (math.exp(-(((x1-mex)**2)/sdx + ((y1-mey)**2)/sdy) )) + max(0,-const) - max(0, const)) - xl = np.linspace(0,1) + sigmoid = lambda x1, y1: ( + const * (math.exp(-(((x1 - mex) ** 2) / sdx + ((y1 - mey) ** 2) / sdy))) + + max(0, -const) + - max(0, const) + ) + xl = np.linspace(0, 1) yl = np.linspace(0, 1) X, Y = np.meshgrid(xl, yl) @@ -1632,11 +1777,17 @@ def sigmoidf(x, y, sdx=0.05, sdy=0.05, mex=0.5, mey=0.5, const=1): mino = np.amin(Z) maxo = np.amax(Z) res = sigmoid(x, y) - res = max(((((res - mino) * (1 - 0)) / (maxo - mino)) + 0), 0.01)*self.magnitude + res = max(((((res - mino) * (1 - 0)) / (maxo - mino)) + 0), 0.01) * self.magnitude return res def corner(x, y, corner="ul", method="out", sdx=0.05, sdy=0.05, mex=0.5, mey=0.5): - ll = {'dr': (0, 0.5, 0, 0.5), 'dl': (0.5, 1, 0, 0.5), 'ur': (0, 0.5, 0.5, 1), 'ul': (0.5, 1, 0.5, 1), 'bell': (0, 1, 0, 1)} + ll = { + 'dr': (0, 0.5, 0, 0.5), + 'dl': (0.5, 1, 0, 0.5), + 'ur': (0, 0.5, 0.5, 1), + 'ul': (0.5, 1, 0.5, 1), + 'bell': (0, 1, 0, 1), + } new_c = ll[corner] new_x = (((x - 0) * (new_c[1] - new_c[0])) / (1 - 0)) + new_c[0] new_y = (((y - 0) * (new_c[3] - new_c[2])) / (1 - 0)) + new_c[2] @@ -1644,10 +1795,10 @@ def corner(x, y, corner="ul", method="out", sdx=0.05, sdy=0.05, mex=0.5, mey=0.5 const = 1 else: if method == "out": - const =- 1 + const = -1 else: const = 1 - res = sigmoidf(x=new_x, y=new_y,sdx=sdx, sdy=sdy, mex=mex, mey=mey, const=const) + res = sigmoidf(x=new_x, y=new_y, sdx=sdx, sdy=sdy, mex=mex, mey=mey, const=const) return res @@ -1656,31 +1807,28 @@ def do(image): for a, b, c, d in polygon_indices: x1, y1, x2, y2, x3, y3, x4, y4 = polygons[a] - sigmax = corner(x=x3/w, y=y3/h, corner=self.corner, method=self.method, sdx=self.sdx, sdy=self.sdy, mex=self.mex, mey=self.mey) + sigmax = corner( + x=x3 / w, + y=y3 / h, + corner=self.corner, + method=self.method, + sdx=self.sdx, + sdy=self.sdy, + mex=self.mex, + mey=self.mey, + ) dx = np.random.normal(0, sigmax, 1)[0] dy = np.random.normal(0, sigmax, 1)[0] - polygons[a] = [x1, y1, - x2, y2, - x3 + dx, y3 + dy, - x4, y4] + polygons[a] = [x1, y1, x2, y2, x3 + dx, y3 + dy, x4, y4] x1, y1, x2, y2, x3, y3, x4, y4 = polygons[b] - polygons[b] = [x1, y1, - x2 + dx, y2 + dy, - x3, y3, - x4, y4] + polygons[b] = [x1, y1, x2 + dx, y2 + dy, x3, y3, x4, y4] x1, y1, x2, y2, x3, y3, x4, y4 = polygons[c] - polygons[c] = [x1, y1, - x2, y2, - x3, y3, - x4 + dx, y4 + dy] + polygons[c] = [x1, y1, x2, y2, x3, y3, x4 + dx, y4 + dy] x1, y1, x2, y2, x3, y3, x4, y4 = polygons[d] - polygons[d] = [x1 + dx, y1 + dy, - x2, y2, - x3, y3, - x4, y4] + polygons[d] = [x1 + dx, y1 + dy, x2, y2, x3, y3, x4, y4] generated_mesh = [] for i in range(len(dimensions)): @@ -1701,7 +1849,8 @@ class Zoom(Operation): This class is used to enlarge images (to zoom) but to return a cropped region of the zoomed image of the same size as the original image. """ - def __init__(self, probability, min_factor, max_factor): + + def __init__(self, probability, skip_gt_image, min_factor, max_factor): """ The amount of zoom applied is randomised, from between :attr:`min_factor` and :attr:`max_factor`. Set these both to the same @@ -1719,7 +1868,7 @@ def __init__(self, probability, min_factor, max_factor): :type min_factor: Float :type max_factor: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.min_factor = min_factor self.max_factor = max_factor @@ -1737,15 +1886,20 @@ def perform_operation(self, images): def do(image): w, h = image.size - image_zoomed = image.resize((int(round(image.size[0] * factor)), - int(round(image.size[1] * factor))), - resample=Image.BICUBIC) + image_zoomed = image.resize( + (int(round(image.size[0] * factor)), int(round(image.size[1] * factor))), + resample=Image.BICUBIC, + ) w_zoomed, h_zoomed = image_zoomed.size - return image_zoomed.crop((floor((float(w_zoomed) / 2) - (float(w) / 2)), - floor((float(h_zoomed) / 2) - (float(h) / 2)), - floor((float(w_zoomed) / 2) + (float(w) / 2)), - floor((float(h_zoomed) / 2) + (float(h) / 2)))) + return image_zoomed.crop( + ( + floor((float(w_zoomed) / 2) - (float(w) / 2)), + floor((float(h_zoomed) / 2) - (float(h) / 2)), + floor((float(w_zoomed) / 2) + (float(w) / 2)), + floor((float(h_zoomed) / 2) + (float(h) / 2)), + ) + ) augmented_images = [] @@ -1760,7 +1914,7 @@ class ZoomRandom(Operation): This class is used to zoom into random areas of the image. """ - def __init__(self, probability, percentage_area, randomise): + def __init__(self, probability, skip_gt_image, percentage_area, randomise): """ Zooms into a random area of the image, rather than the centre of the image, as is done by :class:`Zoom`. The zoom factor is fixed @@ -1776,7 +1930,7 @@ def __init__(self, probability, percentage_area, randomise): upper bound, and randomises the zoom level from between 0.1 and :attr:`percentage_area`. """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.percentage_area = percentage_area self.randomise = randomise @@ -1803,11 +1957,20 @@ def perform_operation(self, images): w_new = int(floor(w * r_percentage_area)) h_new = int(floor(h * r_percentage_area)) - random_left_shift = random.randint(0, (w - w_new)) # Note: randint() is from uniform distribution. + random_left_shift = random.randint( + 0, (w - w_new) + ) # Note: randint() is from uniform distribution. random_down_shift = random.randint(0, (h - h_new)) def do(image): - image = image.crop((random_left_shift, random_down_shift, w_new + random_left_shift, h_new + random_down_shift)) + image = image.crop( + ( + random_left_shift, + random_down_shift, + w_new + random_left_shift, + h_new + random_down_shift, + ) + ) return image.resize((w, h), resample=Image.BICUBIC) @@ -1823,8 +1986,18 @@ class HSVShifting(Operation): """ CURRENTLY NOT IMPLEMENTED. """ - def __init__(self, probability, hue_shift, saturation_scale, saturation_shift, value_scale, value_shift): - Operation.__init__(self, probability) + + def __init__( + self, + probability, + skip_gt_image, + hue_shift, + saturation_scale, + saturation_shift, + value_scale, + value_shift, + ): + Operation.__init__(self, probability, skip_gt_image) self.hue_shift = hue_shift self.saturation_scale = saturation_scale self.saturation_shift = saturation_shift @@ -1832,19 +2005,20 @@ def __init__(self, probability, hue_shift, saturation_scale, saturation_shift, v self.value_shift = value_shift def perform_operation(self, images): - def do(image): hsv = np.array(image.convert("HSV"), 'float64') - hsv /= 255. + hsv /= 255.0 hsv[..., 0] += np.random.uniform(-self.hue_shift, self.hue_shift) - hsv[..., 1] *= np.random.uniform(1 / (1 + self.saturation_scale), 1 + self.saturation_scale) + hsv[..., 1] *= np.random.uniform( + 1 / (1 + self.saturation_scale), 1 + self.saturation_scale + ) hsv[..., 1] += np.random.uniform(-self.saturation_shift, self.saturation_shift) hsv[..., 2] *= np.random.uniform(1 / (1 + self.value_scale), 1 + self.value_scale) hsv[..., 2] += np.random.uniform(-self.value_shift, self.value_shift) hsv.clip(0, 1, hsv) - hsv = np.uint8(np.round(hsv * 255.)) + hsv = np.uint8(np.round(hsv * 255.0)) return Image.fromarray(hsv, "HSV").convert("RGB") @@ -1869,7 +2043,8 @@ class RandomErasing(Operation): Random Erasing can make a trained neural network more robust to occlusion. """ - def __init__(self, probability, rectangle_area): + + def __init__(self, probability, skip_gt_image, rectangle_area): """ The size of the random rectangle is controlled using the :attr:`rectangle_area` parameter. This area is random in its @@ -1879,7 +2054,7 @@ def __init__(self, probability, rectangle_area): performed. :param rectangle_area: The percentage are of the image to occlude. """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.rectangle_area = rectangle_area def perform_operation(self, images): @@ -1907,9 +2082,13 @@ def do(image): h_occlusion = random.randint(h_occlusion_min, h_occlusion_max) if len(image.getbands()) == 1: - rectangle = Image.fromarray(np.uint8(np.random.rand(w_occlusion, h_occlusion) * 255)) + rectangle = Image.fromarray( + np.uint8(np.random.rand(w_occlusion, h_occlusion) * 255) + ) else: - rectangle = Image.fromarray(np.uint8(np.random.rand(w_occlusion, h_occlusion, len(image.getbands())) * 255)) + rectangle = Image.fromarray( + np.uint8(np.random.rand(w_occlusion, h_occlusion, len(image.getbands())) * 255) + ) random_position_x = random.randint(0, w - w_occlusion) random_position_y = random.randint(0, h - h_occlusion) @@ -1931,7 +2110,8 @@ class Custom(Operation): Class that allows for a custom operation to be performed using Augmentor's standard :class:`~Augmentor.Pipeline.Pipeline` object. """ - def __init__(self, probability, custom_function, **function_arguments): + + def __init__(self, probability, skip_gt_image, custom_function, **function_arguments): """ Creates a custom operation that can be added to a pipeline. @@ -1953,7 +2133,7 @@ def __init__(self, probability, custom_function, **function_arguments): :type custom_function: \*Function :type function_arguments: dict """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.custom_function = custom_function self.function_arguments = function_arguments @@ -1977,7 +2157,8 @@ class ZoomGroundTruth(Operation): This class is used to enlarge images (to zoom) but to return a cropped region of the zoomed image of the same size as the original image. """ - def __init__(self, probability, min_factor, max_factor): + + def __init__(self, probability, skip_gt_image, min_factor, max_factor): """ The amount of zoom applied is randomised, from between :attr:`min_factor` and :attr:`max_factor`. Set these both to the same @@ -1995,7 +2176,7 @@ def __init__(self, probability, min_factor, max_factor): :type min_factor: Float :type max_factor: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.min_factor = min_factor self.max_factor = max_factor @@ -2014,13 +2195,20 @@ def do(image): w, h = image.size # TODO: Join these two functions together so that we don't have this image_zoom variable lying around. - image_zoomed = image.resize((int(round(image.size[0] * factor)), int(round(image.size[1] * factor))), resample=Image.BICUBIC) + image_zoomed = image.resize( + (int(round(image.size[0] * factor)), int(round(image.size[1] * factor))), + resample=Image.BICUBIC, + ) w_zoomed, h_zoomed = image_zoomed.size - return image_zoomed.crop((floor((float(w_zoomed) / 2) - (float(w) / 2)), - floor((float(h_zoomed) / 2) - (float(h) / 2)), - floor((float(w_zoomed) / 2) + (float(w) / 2)), - floor((float(h_zoomed) / 2) + (float(h) / 2)))) + return image_zoomed.crop( + ( + floor((float(w_zoomed) / 2) - (float(w) / 2)), + floor((float(h_zoomed) / 2) - (float(h) / 2)), + floor((float(w_zoomed) / 2) + (float(w) / 2)), + floor((float(h_zoomed) / 2) + (float(h) / 2)), + ) + ) augmented_images = [] @@ -2068,7 +2256,8 @@ class Mixup(Operation): :math:`\\alpha` result in less *mixup* effect whereas larger values would tend to result in overfitting. """ - def __init__(self, probability, alpha=0.4): + + def __init__(self, probability, skip_gt_image, alpha=0.4): """ .. note:: Not yet enabled! @@ -2086,7 +2275,7 @@ def __init__(self, probability, alpha=0.4): :type probability: Float :type alpha: Float """ - Operation.__init__(self, probability) + Operation.__init__(self, probability, skip_gt_image) self.alpha = alpha def perform_operation(self, images): diff --git a/Augmentor/Pipeline.py b/Augmentor/Pipeline.py index 8695c23..b0b4a8e 100644 --- a/Augmentor/Pipeline.py +++ b/Augmentor/Pipeline.py @@ -10,8 +10,7 @@ For a good overview of how to use Augmentor, along with code samples and example images, can be seen in the :ref:`mainfeatures` section. """ -from __future__ import (absolute_import, division, - print_function, unicode_literals) +from __future__ import absolute_import, division, print_function, unicode_literals from PIL.Image import Image from builtins import * @@ -25,6 +24,7 @@ import uuid import warnings import numpy as np + from concurrent.futures import ThreadPoolExecutor # NOTE: @@ -84,10 +84,12 @@ def __init__(self, source_directory=None, output_directory="output", save_format self.process_ground_truth_images = False if source_directory is not None: - self._populate(source_directory=source_directory, - output_directory=output_directory, - ground_truth_directory=None, - ground_truth_output_directory=output_directory) + self._populate( + source_directory=source_directory, + output_directory=output_directory, + ground_truth_directory=None, + ground_truth_output_directory=output_directory, + ) def __call__(self, augmentor_image): """ @@ -104,7 +106,13 @@ def __call__(self, augmentor_image): """ return self._execute(augmentor_image) - def _populate(self, source_directory, output_directory, ground_truth_directory, ground_truth_output_directory): + def _populate( + self, + source_directory, + output_directory, + ground_truth_directory, + ground_truth_output_directory, + ): """ Private method for populating member variables with AugmentorImage objects for each of the images found in the source directory @@ -171,30 +179,36 @@ def _check_images(self, abs_output_directory): try: os.makedirs(abs_output_directory) except IOError: - print("Insufficient rights to read or write output directory (%s)" - % abs_output_directory) + print( + "Insufficient rights to read or write output directory (%s)" + % abs_output_directory + ) else: for class_label in self.class_labels: if not os.path.exists(os.path.join(abs_output_directory, str(class_label[0]))): try: os.makedirs(os.path.join(abs_output_directory, str(class_label[0]))) except IOError: - print("Insufficient rights to read or write output directory (%s)" - % abs_output_directory) + print( + "Insufficient rights to read or write output directory (%s)" + % abs_output_directory + ) for augmentor_image in self.augmentor_images: try: with Image.open(augmentor_image.image_path) as opened_image: self.distinct_dimensions.add(opened_image.size) self.distinct_formats.add(opened_image.format) except IOError as e: - print("There is a problem with image %s in your source directory: %s" - % (augmentor_image.image_path, e.message)) + print( + "There is a problem with image %s in your source directory: %s" + % (augmentor_image.image_path, e.message) + ) self.augmentor_images.remove(augmentor_image) sys.stdout.write("Initialised with %s image(s) found.\n" % len(self.augmentor_images)) sys.stdout.write("Output directory set to %s." % abs_output_directory) - def _execute(self, augmentor_image, save_to_disk=True, multi_threaded=True): + def _execute(self, augmentor_image, save_to_disk=True, multi_threaded=False): """ Private method. Used to pass an image through the current pipeline, and return the augmented image. @@ -211,26 +225,44 @@ def _execute(self, augmentor_image, save_to_disk=True, multi_threaded=True): :return: The augmented image. """ + images_origin = [] + images_pil = [] + images_gt = [] images = [] if augmentor_image.image_path is not None: - images.append(Image.open(augmentor_image.image_path)) + images_origin.append(Image.open(augmentor_image.image_path)) # What if they are array data? if augmentor_image.pil_images is not None: - images.append(augmentor_image.pil_images) + images_pil.append(augmentor_image.pil_images) if augmentor_image.ground_truth is not None: if isinstance(augmentor_image.ground_truth, list): for image in augmentor_image.ground_truth: - images.append(Image.open(image)) + images_gt.append(Image.open(image)) else: - images.append(Image.open(augmentor_image.ground_truth)) + images_gt.append(Image.open(augmentor_image.ground_truth)) + + len_images_origin = len(images_origin) + len_images_pil = len(images_pil) + len_images_gt = len(images_gt) + + images = images_origin + images_pil + images_gt for operation in self.operations: r = round(random.uniform(0, 1), 1) if r <= operation.probability: - images = operation.perform_operation(images) + if operation.skip_gt_image is True: + if len(images) > 0: + images[0 : len_images_origin + len_images_pil] = operation.perform_operation( + images[0 : len_images_origin + len_images_pil] + ) + else: + if len(images) > 0: + images[0 : len_images_origin + len_images_pil + len_images_gt] = operation.perform_operation( + images[0 : len_images_origin + len_images_pil + len_images_gt] + ) # TEMP FOR TESTING # save_to_disk = False @@ -240,34 +272,50 @@ def _execute(self, augmentor_image, save_to_disk=True, multi_threaded=True): try: for i in range(len(images)): if i == 0: - save_name = augmentor_image.class_label \ - + "_original_" \ - + os.path.basename(augmentor_image.image_path) \ - + "_" \ - + file_name \ - + "." \ - + (self.save_format if self.save_format else augmentor_image.file_format) + save_name = ( + augmentor_image.class_label + + "_original_" + + os.path.basename(augmentor_image.image_path) + + "_" + + file_name + + "." + + ( + self.save_format + if self.save_format + else augmentor_image.file_format + ) + ) images[i].save(os.path.join(augmentor_image.output_directory, save_name)) else: - save_name = "_groundtruth_(" \ - + str(i) \ - + ")_" \ - + augmentor_image.class_label \ - + "_" \ - + os.path.basename(augmentor_image.image_path) \ - + "_" \ - + file_name \ - + "." \ - + (self.save_format if self.save_format else augmentor_image.file_format) + save_name = ( + "_groundtruth_(" + + str(i) + + ")_" + + augmentor_image.class_label + + "_" + + os.path.basename(augmentor_image.image_path) + + "_" + + file_name + + "." + + ( + self.save_format + if self.save_format + else augmentor_image.file_format + ) + ) images[i].save(os.path.join(augmentor_image.output_directory, save_name)) except IOError as e: print("Error writing %s, %s. Change save_format to PNG?" % (file_name, e.message)) - print("You can change the save format using the set_save_format(save_format) function.") - print("By passing save_format=\"auto\", Augmentor can save in the correct format automatically.") + print( + "You can change the save format using the set_save_format(save_format) function." + ) + print( + "By passing save_format=\"auto\", Augmentor can save in the correct format automatically." + ) # TODO: Fix this really strange behaviour. # As a workaround, we can pass the same back and basically @@ -323,7 +371,7 @@ def set_save_format(self, save_format): else: self.save_format = save_format - def sample(self, n, multi_threaded=True): + def sample(self, n, multi_threaded=False): """ Generate :attr:`n` number of samples from the current pipeline. @@ -345,9 +393,11 @@ def sample(self, n, multi_threaded=True): :return: None """ if len(self.augmentor_images) == 0: - raise IndexError("There are no images in the pipeline. " - "Add a directory using add_directory(), " - "pointing it to a directory containing images.") + raise IndexError( + "There are no images in the pipeline. " + "Add a directory using add_directory(), " + "pointing it to a directory containing images." + ) if len(self.operations) == 0: raise IndexError("There are no operations associated with this pipeline.") @@ -359,16 +409,22 @@ def sample(self, n, multi_threaded=True): if multi_threaded: # TODO: Restore the functionality (appearance of progress bar) from the pre-multi-thread code above. - with tqdm(total=len(augmentor_images), desc="Executing Pipeline", unit=" Samples") as progress_bar: + with tqdm( + total=len(augmentor_images), desc="Executing Pipeline", unit=" Samples" + ) as progress_bar: with ThreadPoolExecutor(max_workers=None) as executor: for result in executor.map(self, augmentor_images): progress_bar.set_description("Processing %s" % result) progress_bar.update(1) else: - with tqdm(total=len(augmentor_images), desc="Executing Pipeline", unit=" Samples") as progress_bar: + with tqdm( + total=len(augmentor_images), desc="Executing Pipeline", unit=" Samples" + ) as progress_bar: for augmentor_image in augmentor_images: self._execute(augmentor_image) - progress_bar.set_description("Processing %s" % os.path.basename(augmentor_image.image_path)) + progress_bar.set_description( + "Processing %s" % os.path.basename(augmentor_image.image_path) + ) progress_bar.update(1) # This does not work as it did in the pre-multi-threading code above for some reason. @@ -388,7 +444,7 @@ def process(self): :return: None """ - self.sample(0, multi_threaded=True) + self.sample(0, multi_threaded=False) return None @@ -433,7 +489,7 @@ def image_generator(self): warnings.warn("This function has been deprecated.", DeprecationWarning) while True: - im_index = random.randint(0, len(self.augmentor_images)-1) # Fix for issue 52. + im_index = random.randint(0, len(self.augmentor_images) - 1) # Fix for issue 52. yield self._execute(self.augmentor_images[im_index], save_to_disk=False) def generator_threading_tests(self, batch_size): @@ -452,8 +508,10 @@ def generator_threading_tests(self, batch_size): def generator_threading_tests_with_matrix_data(self, images, label): - self.augmentor_images = [AugmentorImage(image_path=None, output_directory=None, pil_images=x, label=y) - for x, y in zip(images, label)] + self.augmentor_images = [ + AugmentorImage(image_path=None, output_directory=None, pil_images=x, label=y) + for x, y in zip(images, label) + ] return 1 @@ -517,8 +575,10 @@ def keras_generator(self, batch_size, scaled=True, image_data_format="channels_l # batch[i:i+28] # Select random image, get image array and label - random_image_index = random.randint(0, len(self.augmentor_images)-1) - numpy_array = np.asarray(self._execute(self.augmentor_images[random_image_index], save_to_disk=False)) + random_image_index = random.randint(0, len(self.augmentor_images) - 1) + numpy_array = np.asarray( + self._execute(self.augmentor_images[random_image_index], save_to_disk=False) + ) label = self.augmentor_images[random_image_index].categorical_label # Reshape @@ -543,11 +603,13 @@ def keras_generator(self, batch_size, scaled=True, image_data_format="channels_l if scaled: X = X.astype('float32') - X /= 255. # PR #126 + X /= 255.0 # PR #126 yield (X, y) - def keras_generator_from_array(self, images, labels, batch_size, scaled=True, image_data_format="channels_last"): + def keras_generator_from_array( + self, images, labels, batch_size, scaled=True, image_data_format="channels_last" + ): """ Returns an image generator that will sample from the current pipeline indefinitely, as long as it is called. @@ -611,7 +673,7 @@ def keras_generator_from_array(self, images, labels, batch_size, scaled=True, im for i in range(batch_size): - random_image_index = random.randint(0, len(images)-1) + random_image_index = random.randint(0, len(images) - 1) # Before passing the image we must format it in a shape that # Pillow can understand, that is either (w, h) for greyscale @@ -631,9 +693,13 @@ def keras_generator_from_array(self, images, labels, batch_size, scaled=True, im h = images[random_image_index].shape[1] if l == 1: - numpy_array = self._execute_with_array(np.reshape(images[random_image_index], (w, h))) + numpy_array = self._execute_with_array( + np.reshape(images[random_image_index], (w, h)) + ) else: - numpy_array = self._execute_with_array(np.reshape(images[random_image_index], (w, h, l))) + numpy_array = self._execute_with_array( + np.reshape(images[random_image_index], (w, h, l)) + ) if image_data_format == "channels_first": numpy_array = numpy_array.reshape(l, w, h) @@ -648,9 +714,9 @@ def keras_generator_from_array(self, images, labels, batch_size, scaled=True, im if scaled: X = X.astype('float32') - X /= 255. # PR #126 + X /= 255.0 # PR #126 - yield(X, y) + yield (X, y) def keras_preprocess_func(self): """ @@ -673,15 +739,17 @@ def keras_preprocess_func(self): :return: The pipeline as a function. """ + def _transform_keras_preprocess_func(image): image = Image.fromarray(np.uint8(255 * image)) for operation in self.operations: r = random.uniform(0, 1) if r < operation.probability: image = operation.perform_operation([image])[0] - #a = AugmentorImage(image_path=None, output_directory=None) - #a.image_PIL = - return image #self._execute(a) + # a = AugmentorImage(image_path=None, output_directory=None) + # a.image_PIL = + return image # self._execute(a) + return _transform_keras_preprocess_func def torch_transform(self): @@ -702,6 +770,7 @@ def torch_transform(self): :return: The pipeline as a function. """ + def _transform(image): for operation in self.operations: r = random.uniform(0, 1) @@ -764,10 +833,12 @@ def add_further_directory(self, new_source_directory, new_output_directory="outp if not os.path.exists(new_source_directory): raise IOError("The path does not appear to exist.") - self._populate(source_directory=new_source_directory, - output_directory=new_output_directory, - ground_truth_directory=None, - ground_truth_output_directory=new_output_directory) + self._populate( + source_directory=new_source_directory, + output_directory=new_output_directory, + ground_truth_directory=None, + ground_truth_output_directory=new_output_directory, + ) def status(self): """ @@ -814,7 +885,7 @@ def status(self): print("Classes: %s" % len(label_pairs)) for label_pair in label_pairs: - print ("\tClass index: %s Class label: %s " % (label_pair[0], label_pair[1])) + print("\tClass index: %s Class label: %s " % (label_pair[0], label_pair[1])) if len(self.augmentor_images) != 0: print("Dimensions: %s" % len(self.distinct_dimensions)) @@ -824,7 +895,9 @@ def status(self): for distinct_format in self.distinct_formats: print("\t %s" % distinct_format) - print("\nYou can remove operations using the appropriate index and the remove_operation(index) function.") + print( + "\nYou can remove operations using the appropriate index and the remove_operation(index) function." + ) @staticmethod def set_seed(seed): @@ -844,7 +917,7 @@ def set_seed(seed): # # https://patrykchrabaszcz.github.io/Imagenet32/ # self.add_operation(Mean(probability=probability)) - def rotate90(self, probability): + def rotate90(self, probability, skip_gt_image=False): """ Rotate an image by 90 degrees. @@ -860,9 +933,11 @@ def rotate90(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Rotate(probability=probability, rotation=90)) + self.add_operation( + Rotate(probability=probability, rotation=90, skip_gt_image=skip_gt_image) + ) - def rotate180(self, probability): + def rotate180(self, probability, skip_gt_image=False): """ Rotate an image by 180 degrees. @@ -878,9 +953,11 @@ def rotate180(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Rotate(probability=probability, rotation=180)) + self.add_operation( + Rotate(probability=probability, rotation=180, skip_gt_image=skip_gt_image) + ) - def rotate270(self, probability): + def rotate270(self, probability, skip_gt_image=False): """ Rotate an image by 270 degrees. @@ -896,9 +973,11 @@ def rotate270(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Rotate(probability=probability, rotation=270)) + self.add_operation( + Rotate(probability=probability, rotation=270, skip_gt_image=skip_gt_image) + ) - def rotate_random_90(self, probability): + def rotate_random_90(self, probability, skip_gt_image=False): """ Rotate an image by either 90, 180, or 270 degrees, selected randomly. @@ -918,9 +997,11 @@ def rotate_random_90(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Rotate(probability=probability, rotation=-1)) + self.add_operation( + Rotate(probability=probability, rotation=-1, skip_gt_image=skip_gt_image) + ) - def rotate(self, probability, max_left_rotation, max_right_rotation): + def rotate(self, probability, max_left_rotation, max_right_rotation, skip_gt_image=False): """ Rotate an image by an arbitrary amount. @@ -960,10 +1041,24 @@ def rotate(self, probability, max_left_rotation, max_right_rotation): if not 0 <= max_right_rotation <= 25: raise ValueError("The max_right_rotation argument must be between 0 and 25.") else: - self.add_operation(RotateRange(probability=probability, max_left_rotation=ceil(max_left_rotation), - max_right_rotation=ceil(max_right_rotation))) - - def rotate_without_crop(self, probability, max_left_rotation, max_right_rotation, expand=False, fillcolor=None): + self.add_operation( + RotateRange( + probability=probability, + max_left_rotation=ceil(max_left_rotation), + max_right_rotation=ceil(max_right_rotation), + skip_gt_image=skip_gt_image, + ) + ) + + def rotate_without_crop( + self, + probability, + max_left_rotation, + max_right_rotation, + expand=False, + fillcolor=None, + skip_gt_image=False, + ): """ Rotate an image without automatically cropping. @@ -990,11 +1085,18 @@ def rotate_without_crop(self, probability, max_left_rotation, max_right_rotation of int numbers. :return: None """ - self.add_operation(RotateStandard(probability=probability, max_left_rotation=ceil(max_left_rotation), - max_right_rotation=ceil(max_right_rotation), expand=expand, - fillcolor=fillcolor)) + self.add_operation( + RotateStandard( + probability=probability, + max_left_rotation=ceil(max_left_rotation), + max_right_rotation=ceil(max_right_rotation), + expand=expand, + fillcolor=fillcolor, + skip_gt_image=skip_gt_image, + ) + ) - def flip_top_bottom(self, probability): + def flip_top_bottom(self, probability, skip_gt_image=False): """ Flip (mirror) the image along its vertical axis, i.e. from top to bottom. @@ -1009,9 +1111,15 @@ def flip_top_bottom(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Flip(probability=probability, top_bottom_left_right="TOP_BOTTOM")) + self.add_operation( + Flip( + probability=probability, + top_bottom_left_right="TOP_BOTTOM", + skip_gt_image=skip_gt_image, + ) + ) - def flip_left_right(self, probability): + def flip_left_right(self, probability, skip_gt_image=False): """ Flip (mirror) the image along its horizontal axis, i.e. from left to right. @@ -1026,9 +1134,15 @@ def flip_left_right(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Flip(probability=probability, top_bottom_left_right="LEFT_RIGHT")) + self.add_operation( + Flip( + probability=probability, + top_bottom_left_right="LEFT_RIGHT", + skip_gt_image=skip_gt_image, + ) + ) - def flip_random(self, probability): + def flip_random(self, probability, skip_gt_image=False): """ Flip (mirror) the image along **either** its horizontal or vertical axis. @@ -1044,9 +1158,17 @@ def flip_random(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Flip(probability=probability, top_bottom_left_right="RANDOM")) - - def random_distortion(self, probability, grid_width, grid_height, magnitude): + self.add_operation( + Flip( + probability=probability, + top_bottom_left_right="RANDOM", + skip_gt_image=skip_gt_image, + ) + ) + + def random_distortion( + self, probability, grid_width, grid_height, magnitude, skip_gt_image=False + ): """ Performs a random, elastic distortion on an image. @@ -1078,11 +1200,30 @@ def random_distortion(self, probability, grid_width, grid_height, magnitude): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Distort(probability=probability, grid_width=grid_width, - grid_height=grid_height, magnitude=magnitude)) - - def gaussian_distortion(self, probability, grid_width, grid_height, magnitude, corner, method, mex=0.5, mey=0.5, - sdx=0.05, sdy=0.05): + self.add_operation( + Distort( + probability=probability, + grid_width=grid_width, + grid_height=grid_height, + magnitude=magnitude, + skip_gt_image=skip_gt_image, + ) + ) + + def gaussian_distortion( + self, + probability, + grid_width, + grid_height, + magnitude, + corner, + method, + mex=0.5, + mey=0.5, + sdx=0.05, + sdy=0.05, + skip_gt_image=False, + ): """ Performs a random, elastic gaussian distortion on an image. @@ -1140,13 +1281,23 @@ def gaussian_distortion(self, probability, grid_width, grid_height, magnitude, c if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(GaussianDistortion(probability=probability, grid_width=grid_width, - grid_height=grid_height, - magnitude=magnitude, corner=corner, - method=method, mex=mex, - mey=mey, sdx=sdx, sdy=sdy)) - - def zoom(self, probability, min_factor, max_factor): + self.add_operation( + GaussianDistortion( + probability=probability, + grid_width=grid_width, + grid_height=grid_height, + magnitude=magnitude, + corner=corner, + method=method, + mex=mex, + mey=mey, + sdx=sdx, + sdy=sdy, + skip_gt_image=skip_gt_image, + ) + ) + + def zoom(self, probability, min_factor, max_factor, skip_gt_image=False): """ Zoom in to an image, while **maintaining its size**. The amount by which the image is zoomed is a randomly chosen value between @@ -1174,9 +1325,18 @@ def zoom(self, probability, min_factor, max_factor): elif min_factor <= 0: raise ValueError("The min_factor argument must be greater than 0.") else: - self.add_operation(Zoom(probability=probability, min_factor=min_factor, max_factor=max_factor)) - - def zoom_random(self, probability, percentage_area, randomise_percentage_area=False): + self.add_operation( + Zoom( + probability=probability, + min_factor=min_factor, + max_factor=max_factor, + skip_gt_image=skip_gt_image, + ) + ) + + def zoom_random( + self, probability, percentage_area, randomise_percentage_area=False, skip_gt_image=False, + ): """ Zooms into an image at a random location within the image. @@ -1197,13 +1357,22 @@ def zoom_random(self, probability, percentage_area, randomise_percentage_area=Fa if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0.1 <= percentage_area < 1: - raise ValueError("The percentage_area argument must be greater than 0.1 and less than 1.") + raise ValueError( + "The percentage_area argument must be greater than 0.1 and less than 1." + ) elif not isinstance(randomise_percentage_area, bool): raise ValueError("The randomise_percentage_area argument must be True or False.") else: - self.add_operation(ZoomRandom(probability=probability, percentage_area=percentage_area, randomise=randomise_percentage_area)) + self.add_operation( + ZoomRandom( + probability=probability, + percentage_area=percentage_area, + randomise=randomise_percentage_area, + skip_gt_image=skip_gt_image, + ) + ) - def crop_by_size(self, probability, width, height, centre=True): + def crop_by_size(self, probability, width, height, centre=True, skip_gt_image=False): """ Crop an image according to a set of dimensions. @@ -1239,9 +1408,19 @@ def crop_by_size(self, probability, width, height, centre=True): elif not isinstance(centre, bool): raise ValueError("The centre argument must be True or False.") else: - self.add_operation(Crop(probability=probability, width=width, height=height, centre=centre)) - - def crop_centre(self, probability, percentage_area, randomise_percentage_area=False): + self.add_operation( + Crop( + probability=probability, + width=width, + height=height, + centre=centre, + skip_gt_image=skip_gt_image, + ) + ) + + def crop_centre( + self, probability, percentage_area, randomise_percentage_area=False, skip_gt_image=False, + ): """ Crops the centre of an image as a percentage of the image's area. @@ -1260,14 +1439,25 @@ def crop_centre(self, probability, percentage_area, randomise_percentage_area=Fa if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0.1 <= percentage_area < 1: - raise ValueError("The percentage_area argument must be greater than 0.1 and less than 1.") + raise ValueError( + "The percentage_area argument must be greater than 0.1 and less than 1." + ) elif not isinstance(randomise_percentage_area, bool): raise ValueError("The randomise_percentage_area argument must be True or False.") else: - self.add_operation(CropPercentage(probability=probability, percentage_area=percentage_area, centre=True, - randomise_percentage_area=randomise_percentage_area)) - - def crop_random(self, probability, percentage_area, randomise_percentage_area=False): + self.add_operation( + CropPercentage( + probability=probability, + percentage_area=percentage_area, + centre=True, + randomise_percentage_area=randomise_percentage_area, + skip_gt_image=skip_gt_image, + ) + ) + + def crop_random( + self, probability, percentage_area, randomise_percentage_area=False, skip_gt_image=False, + ): """ Crop a random area of an image, based on the percentage area to be returned. @@ -1290,14 +1480,23 @@ def crop_random(self, probability, percentage_area, randomise_percentage_area=Fa if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0.1 <= percentage_area < 1: - raise ValueError("The percentage_area argument must be greater than 0.1 and less than 1.") + raise ValueError( + "The percentage_area argument must be greater than 0.1 and less than 1." + ) elif not isinstance(randomise_percentage_area, bool): raise ValueError("The randomise_percentage_area argument must be True or False.") else: - self.add_operation(CropPercentage(probability=probability, percentage_area=percentage_area, centre=False, - randomise_percentage_area=randomise_percentage_area)) - - def histogram_equalisation(self, probability=1.0): + self.add_operation( + CropPercentage( + probability=probability, + percentage_area=percentage_area, + centre=False, + randomise_percentage_area=randomise_percentage_area, + skip_gt_image=skip_gt_image, + ) + ) + + def histogram_equalisation(self, probability=1.0, skip_gt_image=False): """ Apply histogram equalisation to the image. @@ -1310,9 +1509,11 @@ def histogram_equalisation(self, probability=1.0): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(HistogramEqualisation(probability=probability)) + self.add_operation( + HistogramEqualisation(probability=probability, skip_gt_image=skip_gt_image) + ) - def scale(self, probability, scale_factor): + def scale(self, probability, scale_factor, skip_gt_image=False): """ Scale (enlarge) an image, while maintaining its aspect ratio. This returns an image with larger dimensions than the original image. @@ -1332,9 +1533,13 @@ def scale(self, probability, scale_factor): elif scale_factor <= 1.0: raise ValueError("The scale_factor argument must be greater than 1.") else: - self.add_operation(Scale(probability=probability, scale_factor=scale_factor)) + self.add_operation( + Scale( + probability=probability, scale_factor=scale_factor, skip_gt_image=skip_gt_image + ) + ) - def resize(self, probability, width, height, resample_filter="BICUBIC"): + def resize(self, probability, width, height, resample_filter="BICUBIC", skip_gt_image=False): """ Resize an image according to a set of dimensions specified by the user in pixels. @@ -1359,11 +1564,21 @@ def resize(self, probability, width, height, resample_filter="BICUBIC"): elif not height > 1: raise ValueError("Height must be greater than 1.") elif resample_filter not in Pipeline._legal_filters: - raise ValueError("The save_filter argument must be one of %s." % Pipeline._legal_filters) + raise ValueError( + "The save_filter argument must be one of %s." % Pipeline._legal_filters + ) else: - self.add_operation(Resize(probability=probability, width=width, height=height, resample_filter=resample_filter)) - - def skew_left_right(self, probability, magnitude=1): + self.add_operation( + Resize( + probability=probability, + width=width, + height=height, + resample_filter=resample_filter, + skip_gt_image=skip_gt_image, + ) + ) + + def skew_left_right(self, probability, magnitude=1, skip_gt_image=False): """ Skew an image by tilting it left or right by a random amount. The magnitude of this skew can be set to a maximum using the @@ -1383,11 +1598,20 @@ def skew_left_right(self, probability, magnitude=1): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0 < magnitude <= 1: - raise ValueError("The magnitude argument must be greater than 0 and less than or equal to 1.") + raise ValueError( + "The magnitude argument must be greater than 0 and less than or equal to 1." + ) else: - self.add_operation(Skew(probability=probability, skew_type="TILT_LEFT_RIGHT", magnitude=magnitude)) + self.add_operation( + Skew( + probability=probability, + skew_type="TILT_LEFT_RIGHT", + magnitude=magnitude, + skip_gt_image=skip_gt_image, + ) + ) - def skew_top_bottom(self, probability, magnitude=1): + def skew_top_bottom(self, probability, magnitude=1, skip_gt_image=False): """ Skew an image by tilting it forwards or backwards by a random amount. The magnitude of this skew can be set to a maximum using the @@ -1407,13 +1631,20 @@ def skew_top_bottom(self, probability, magnitude=1): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0 < magnitude <= 1: - raise ValueError("The magnitude argument must be greater than 0 and less than or equal to 1.") + raise ValueError( + "The magnitude argument must be greater than 0 and less than or equal to 1." + ) else: - self.add_operation(Skew(probability=probability, - skew_type="TILT_TOP_BOTTOM", - magnitude=magnitude)) + self.add_operation( + Skew( + probability=probability, + skew_type="TILT_TOP_BOTTOM", + magnitude=magnitude, + skip_gt_image=skip_gt_image, + ) + ) - def skew_tilt(self, probability, magnitude=1): + def skew_tilt(self, probability, magnitude=1, skip_gt_image=False): """ Skew an image by tilting in a random direction, either forwards, backwards, left, or right, by a random amount. The magnitude of @@ -1434,13 +1665,20 @@ def skew_tilt(self, probability, magnitude=1): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0 < magnitude <= 1: - raise ValueError("The magnitude argument must be greater than 0 and less than or equal to 1.") + raise ValueError( + "The magnitude argument must be greater than 0 and less than or equal to 1." + ) else: - self.add_operation(Skew(probability=probability, - skew_type="TILT", - magnitude=magnitude)) + self.add_operation( + Skew( + probability=probability, + skew_type="TILT", + magnitude=magnitude, + skip_gt_image=skip_gt_image, + ) + ) - def skew_corner(self, probability, magnitude=1): + def skew_corner(self, probability, magnitude=1, skip_gt_image=False): """ Skew an image towards one corner, randomly by a random magnitude. @@ -1455,13 +1693,20 @@ def skew_corner(self, probability, magnitude=1): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0 < magnitude <= 1: - raise ValueError("The magnitude argument must be greater than 0 and less than or equal to 1.") + raise ValueError( + "The magnitude argument must be greater than 0 and less than or equal to 1." + ) else: - self.add_operation(Skew(probability=probability, - skew_type="CORNER", - magnitude=magnitude)) + self.add_operation( + Skew( + probability=probability, + skew_type="CORNER", + magnitude=magnitude, + skip_gt_image=skip_gt_image, + ) + ) - def skew(self, probability, magnitude=1): + def skew(self, probability, magnitude=1, skip_gt_image=False): """ Skew an image in a random direction, either left to right, top to bottom, or one of 8 corner directions. @@ -1479,13 +1724,20 @@ def skew(self, probability, magnitude=1): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) elif not 0 < magnitude <= 1: - raise ValueError("The magnitude argument must be greater than 0 and less than or equal to 1.") + raise ValueError( + "The magnitude argument must be greater than 0 and less than or equal to 1." + ) else: - self.add_operation(Skew(probability=probability, - skew_type="RANDOM", - magnitude=magnitude)) + self.add_operation( + Skew( + probability=probability, + skew_type="RANDOM", + magnitude=magnitude, + skip_gt_image=skip_gt_image, + ) + ) - def shear(self, probability, max_shear_left, max_shear_right): + def shear(self, probability, max_shear_left, max_shear_right, skip_gt_image=False): """ Shear the image by a specified number of degrees. @@ -1508,11 +1760,16 @@ def shear(self, probability, max_shear_left, max_shear_right): elif not 0 < max_shear_right <= 25: raise ValueError("The max_shear_right argument must be between 0 and 25.") else: - self.add_operation(Shear(probability=probability, - max_shear_left=max_shear_left, - max_shear_right=max_shear_right)) + self.add_operation( + Shear( + probability=probability, + max_shear_left=max_shear_left, + max_shear_right=max_shear_right, + skip_gt_image=skip_gt_image, + ) + ) - def greyscale(self, probability): + def greyscale(self, probability, skip_gt_image=False): """ Convert images to greyscale. For this operation, setting the :attr:`probability` to 1.0 is recommended. @@ -1528,9 +1785,9 @@ def greyscale(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Greyscale(probability=probability)) + self.add_operation(Greyscale(probability=probability, skip_gt_image=skip_gt_image)) - def black_and_white(self, probability, threshold=128): + def black_and_white(self, probability, threshold=128, skip_gt_image=False): """ Convert images to black and white. In other words convert the image to use a 1-bit, binary palette. The threshold defaults to 128, @@ -1554,9 +1811,13 @@ def black_and_white(self, probability, threshold=128): elif not 0 <= threshold <= 255: raise ValueError("The threshold must be between 0 and 255.") else: - self.add_operation(BlackAndWhite(probability=probability, threshold=threshold)) + self.add_operation( + BlackAndWhite( + probability=probability, threshold=threshold, skip_gt_image=skip_gt_image + ) + ) - def invert(self, probability): + def invert(self, probability, skip_gt_image=False): """ Invert an image. For this operation, setting the :attr:`probability` to 1.0 is recommended. @@ -1572,9 +1833,9 @@ def invert(self, probability): if not 0 < probability <= 1: raise ValueError(Pipeline._probability_error_text) else: - self.add_operation(Invert(probability=probability)) + self.add_operation(Invert(probability=probability, skip_gt_image=skip_gt_image)) - def random_brightness(self,probability,min_factor,max_factor): + def random_brightness(self, probability, min_factor, max_factor, skip_gt_image=False): """ Random change brightness of an image. @@ -1593,9 +1854,16 @@ def random_brightness(self,probability,min_factor,max_factor): elif not min_factor <= max_factor: raise ValueError("The max_factor must be bigger min_factor.") else: - self.add_operation(RandomBrightness(probability=probability, min_factor=min_factor,max_factor=max_factor)) + self.add_operation( + RandomBrightness( + probability=probability, + min_factor=min_factor, + max_factor=max_factor, + skip_gt_image=skip_gt_image, + ) + ) - def random_color(self,probability,min_factor,max_factor): + def random_color(self, probability, min_factor, max_factor, skip_gt_image=False): """ Random change saturation of an image. @@ -1614,9 +1882,16 @@ def random_color(self,probability,min_factor,max_factor): elif not min_factor <= max_factor: raise ValueError("The max_factor must be bigger min_factor.") else: - self.add_operation(RandomColor(probability=probability, min_factor=min_factor,max_factor=max_factor)) + self.add_operation( + RandomColor( + probability=probability, + min_factor=min_factor, + max_factor=max_factor, + skip_gt_image=skip_gt_image, + ) + ) - def random_contrast(self,probability,min_factor,max_factor): + def random_contrast(self, probability, min_factor, max_factor, skip_gt_image=False): """ Random change image contrast. @@ -1635,9 +1910,16 @@ def random_contrast(self,probability,min_factor,max_factor): elif not min_factor <= max_factor: raise ValueError("The max_factor must be bigger min_factor.") else: - self.add_operation(RandomContrast(probability=probability, min_factor=min_factor,max_factor=max_factor)) + self.add_operation( + RandomContrast( + probability=probability, + min_factor=min_factor, + max_factor=max_factor, + skip_gt_image=skip_gt_image, + ) + ) - def random_erasing(self, probability, rectangle_area): + def random_erasing(self, probability, rectangle_area, skip_gt_image=False): """ Work in progress. This operation performs a Random Erasing operation, as described in @@ -1667,7 +1949,13 @@ def random_erasing(self, probability, rectangle_area): elif not 0.01 <= rectangle_area <= 1: raise ValueError("The rectangle_area must be between 0.01 and 1.") else: - self.add_operation(RandomErasing(probability=probability, rectangle_area=rectangle_area)) + self.add_operation( + RandomErasing( + probability=probability, + rectangle_area=rectangle_area, + skip_gt_image=skip_gt_image, + ) + ) def ground_truth(self, ground_truth_directory): """ @@ -1705,29 +1993,41 @@ def ground_truth(self, ground_truth_directory): num_of_ground_truth_images_added = 0 # Progress bar - progress_bar = tqdm(total=len(self.augmentor_images), desc="Processing", unit=' Images', leave=False) + progress_bar = tqdm( + total=len(self.augmentor_images), desc="Processing", unit=' Images', leave=False + ) if len(self.class_labels) == 1: for augmentor_image_idx in range(len(self.augmentor_images)): - ground_truth_image = os.path.join(ground_truth_directory, - self.augmentor_images[augmentor_image_idx].image_file_name) + ground_truth_image = os.path.join( + ground_truth_directory, + self.augmentor_images[augmentor_image_idx].image_file_name, + ) if os.path.isfile(ground_truth_image): self.augmentor_images[augmentor_image_idx].ground_truth = ground_truth_image num_of_ground_truth_images_added += 1 else: for i in range(len(self.class_labels)): for augmentor_image_idx in range(len(self.augmentor_images)): - ground_truth_image = os.path.join(ground_truth_directory, - self.augmentor_images[augmentor_image_idx].class_label, - self.augmentor_images[augmentor_image_idx].image_file_name) + ground_truth_image = os.path.join( + ground_truth_directory, + self.augmentor_images[augmentor_image_idx].class_label, + self.augmentor_images[augmentor_image_idx].image_file_name, + ) if os.path.isfile(ground_truth_image): - if self.augmentor_images[augmentor_image_idx].class_label == self.class_labels[i][0]: + if ( + self.augmentor_images[augmentor_image_idx].class_label + == self.class_labels[i][0] + ): # Check files are the same size. There may be a better way to do this. - original_image_dimensions = \ - Image.open(self.augmentor_images[augmentor_image_idx].image_path).size + original_image_dimensions = Image.open( + self.augmentor_images[augmentor_image_idx].image_path + ).size ground_image_dimensions = Image.open(ground_truth_image).size if original_image_dimensions == ground_image_dimensions: - self.augmentor_images[augmentor_image_idx].ground_truth = ground_truth_image + self.augmentor_images[ + augmentor_image_idx + ].ground_truth = ground_truth_image num_of_ground_truth_images_added += 1 progress_bar.update(1) @@ -1751,14 +2051,19 @@ def get_ground_truth_paths(self): paths = [] for augmentor_image in self.augmentor_images: - print("Image path: %s\nGround truth path: %s\n---\n" % (augmentor_image.image_path, augmentor_image.ground_truth)) + print( + "Image path: %s\nGround truth path: %s\n---\n" + % (augmentor_image.image_path, augmentor_image.ground_truth) + ) paths.append((augmentor_image.image_path, augmentor_image.ground_truth)) return paths class DataFramePipeline(Pipeline): - def __init__(self, source_dataframe, image_col, category_col, output_directory="output", save_format=None): + def __init__( + self, source_dataframe, image_col, category_col, output_directory="output", save_format=None + ): """ Create a new Pipeline object pointing to dataframe containing the paths to your original image dataset. @@ -1775,16 +2080,18 @@ def __init__(self, source_dataframe, image_col, category_col, output_directory=" GIF. :return: A :class:`Pipeline` object. """ - super(DataFramePipeline, self).__init__(source_directory=None, - output_directory=output_directory, - save_format=save_format) + super(DataFramePipeline, self).__init__( + source_directory=None, output_directory=output_directory, save_format=save_format + ) self._populate(source_dataframe, image_col, category_col, output_directory, save_format) def _populate(self, source_dataframe, image_col, category_col, output_directory, save_format): # Assume we have an absolute path for the output # Scan the directory that user supplied. - self.augmentor_images, self.class_labels = scan_dataframe(source_dataframe, image_col, category_col, output_directory) + self.augmentor_images, self.class_labels = scan_dataframe( + source_dataframe, image_col, category_col, output_directory + ) self._check_images(output_directory) @@ -1846,7 +2153,7 @@ def augmentor_images(self): @augmentor_images.setter def augmentor_images(self, value): - self._augmentor_images = value + self._augmentor_images = value @property def labels(self): diff --git a/README.md b/README.md index 1c01bfa..55e3dbe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +自用版本。更改了源码,现在可以单独选择某个operation是否同时作用于原图和label图。调用时添加 skip_gt_image=True 参数即可。如: +p.random_contrast(probability=1, min_factor=0.5, max_factor=1.52, skip_gt_image=True) + + + ![AugmentorLogo](https://github.com/mdbloice/AugmentorFiles/blob/master/Misc/AugmentorLogo.png) Augmentor is an image augmentation library in Python for machine learning. It aims to be a standalone library that is platform and framework independent, which is more convenient, allows for finer grained control over augmentation, and implements the most real-world relevant augmentation techniques. It employs a stochastic approach using building blocks that allow for operations to be pieced together in a pipeline.