Skip to content

Commit 0b0be97

Browse files
jbittonfacebook-github-bot
authored andcommitted
Add split_and_shuffle augmentation to AugLy (#255)
Summary: Pull Request resolved: #255 Adding a split and shuffle attack :) augmentation description: Splits the image into a grid of tiles (determined by n_columns and n_rows) and shuffles the tiles randomly. The resulting image is the concatenation of the shuffled tiles into the same grid format (resulting in an image of the same size) Reviewed By: vitoralbiero Differential Revision: D68475586
1 parent d710e92 commit 0b0be97

File tree

8 files changed

+181
-0
lines changed

8 files changed

+181
-0
lines changed

augly/image/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
sharpen,
4545
shuffle_pixels,
4646
skew,
47+
split_and_shuffle,
4748
vflip,
4849
)
4950
from augly.image.helpers import aug_np_wrapper
@@ -126,6 +127,7 @@
126127
Sharpen,
127128
ShufflePixels,
128129
Skew,
130+
SplitAndShuffle,
129131
VFlip,
130132
)
131133

@@ -174,6 +176,7 @@
174176
"Sharpen",
175177
"ShufflePixels",
176178
"Skew",
179+
"SplitAndShuffle",
177180
"VFlip",
178181
"apply_lambda",
179182
"apply_pil_filter",
@@ -211,6 +214,7 @@
211214
"sharpen",
212215
"shuffle_pixels",
213216
"skew",
217+
"split_and_shuffle",
214218
"vflip",
215219
"apply_lambda_intensity",
216220
"apply_pil_filter_intensity",

augly/image/functional.py

+87
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pickle
1414
import random
1515
from copy import deepcopy
16+
from itertools import product
1617
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
1718

1819
import numpy as np
@@ -2549,6 +2550,92 @@ def skew(
25492550
return imutils.ret_and_save_image(aug_image, output_path, src_mode)
25502551

25512552

2553+
def split_and_shuffle(
2554+
image: Union[str, Image.Image],
2555+
output_path: Optional[str] = None,
2556+
n_columns: int = 3,
2557+
n_rows: int = 3,
2558+
seed: int = 10,
2559+
metadata: Optional[List[Dict[str, Any]]] = None,
2560+
bboxes: Optional[List[Tuple]] = None,
2561+
bbox_format: Optional[str] = None,
2562+
) -> Image.Image:
2563+
"""
2564+
Splits the image into a grid of tiles (determined by n_columns and n_rows) and
2565+
shuffles the tiles randomly. The resulting image is the concatenation of the
2566+
shuffled tiles into the same grid format (resulting in an image of the same size)
2567+
2568+
@param image: the path to an image or a variable of type PIL.Image.Image
2569+
to be augmented
2570+
2571+
@param output_path: the path in which the resulting image will be stored.
2572+
If None, the resulting PIL Image will still be returned
2573+
2574+
@param n_columns: number of columns to split the image into
2575+
2576+
@param n_rows: number of rows to split the image into
2577+
2578+
@param seed: seed for numpy random generator to select random order for shuffling
2579+
2580+
@param metadata: if set to be a list, metadata about the function execution
2581+
including its name, the source & dest width, height, etc. will be appended
2582+
to the inputted list. If set to None, no metadata will be appended or returned
2583+
2584+
@param bboxes: a list of bounding boxes can be passed in here if desired. If
2585+
provided, this list will be modified in place such that each bounding box is
2586+
transformed according to this function
2587+
2588+
@param bbox_format: signifies what bounding box format was used in `bboxes`. Must
2589+
specify `bbox_format` if `bboxes` is provided. Supported bbox_format values are
2590+
"pascal_voc", "pascal_voc_norm", "coco", and "yolo"
2591+
2592+
@returns: the augmented PIL Image
2593+
"""
2594+
np.random.seed(seed)
2595+
2596+
image = imutils.validate_and_load_image(image)
2597+
2598+
assert n_columns > 0, "Expected 'n_columns' to be a positive integer"
2599+
assert n_rows > 0, "Expected 'n_rows' to be a positive integer"
2600+
2601+
func_kwargs = imutils.get_func_kwargs(metadata, locals())
2602+
src_mode = image.mode
2603+
2604+
width, height = image.size
2605+
width_per_tile = width // n_columns
2606+
height_per_tile = height // n_rows
2607+
2608+
grid = product(
2609+
range(0, height - height % height_per_tile, height_per_tile),
2610+
range(0, width - width % width_per_tile, width_per_tile),
2611+
)
2612+
2613+
sub_images = []
2614+
for y0, x0 in grid:
2615+
bbox = (x0, y0, x0 + width_per_tile, y0 + height_per_tile)
2616+
sub_images.append(image.crop(bbox))
2617+
2618+
if len(sub_images) == 2:
2619+
sub_images[0], sub_images[1] = sub_images[1], sub_images[0]
2620+
else:
2621+
np.random.shuffle(sub_images)
2622+
2623+
aug_image = Image.new("RGB", (width_per_tile * n_columns, height_per_tile * n_rows))
2624+
for i, sub_image in enumerate(sub_images):
2625+
x = i % n_columns
2626+
y = i // n_columns
2627+
aug_image.paste(sub_image, (x * width_per_tile, y * height_per_tile))
2628+
2629+
imutils.get_metadata(
2630+
metadata=metadata,
2631+
function_name="split_and_shuffle",
2632+
aug_image=aug_image,
2633+
**func_kwargs,
2634+
)
2635+
2636+
return imutils.ret_and_save_image(aug_image, output_path, src_mode)
2637+
2638+
25522639
def vflip(
25532640
image: Union[str, Image.Image],
25542641
output_path: Optional[str] = None,

augly/image/intensity.py

+7
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,13 @@ def skew_intensity(skew_factor: float, **kwargs) -> float:
315315
return min((abs(skew_factor) / max_skew_factor) * 100.0, 100.0)
316316

317317

318+
def split_and_shuffle_intensity(n_columns: int, n_rows: int, **kwargs) -> float:
319+
assert n_columns > 0, "Expected 'n_columns' to be a positive integer"
320+
assert n_rows > 0, "Expected 'n_rows' to be a positive integer"
321+
322+
return min((1 - (1 / (n_columns * n_rows))) * 100.0, 100.0)
323+
324+
318325
def vflip_intensity(**kwargs) -> float:
319326
return 100.0
320327

augly/image/transforms.py

+58
Original file line numberDiff line numberDiff line change
@@ -2207,6 +2207,64 @@ def apply_transform(
22072207
)
22082208

22092209

2210+
class SplitAndShuffle(BaseTransform):
2211+
def __init__(
2212+
self, n_columns: int = 3, n_rows: int = 3, seed: int = 10, p: float = 1.0
2213+
):
2214+
"""
2215+
@param n_columns: number of columns to split the image into
2216+
2217+
@param n_rows: number of rows to split the image into
2218+
2219+
@param seed: seed for numpy random generator to select random order
2220+
for shuffling
2221+
2222+
@param p: the probability of the transform being applied; default value is 1.0
2223+
"""
2224+
super().__init__(p)
2225+
self.n_columns = n_columns
2226+
self.n_rows = n_rows
2227+
self.seed = seed
2228+
2229+
def apply_transform(
2230+
self,
2231+
image: Image.Image,
2232+
metadata: Optional[List[Dict[str, Any]]] = None,
2233+
bboxes: Optional[List[Tuple]] = None,
2234+
bbox_format: Optional[str] = None,
2235+
) -> Image.Image:
2236+
"""
2237+
Splits the image into a grid of tiles (determined by n_columns and n_rows) and
2238+
shuffles the tiles randomly. The resulting image is the concatenation of the
2239+
shuffled tiles into the same grid format (resulting in an image of the same size)
2240+
2241+
@param image: PIL Image to be augmented
2242+
2243+
@param metadata: if set to be a list, metadata about the function execution
2244+
including its name, the source & dest width, height, etc. will be appended to
2245+
the inputted list. If set to None, no metadata will be appended or returned
2246+
2247+
@param bboxes: a list of bounding boxes can be passed in here if desired. If
2248+
provided, this list will be modified in place such that each bounding box is
2249+
transformed according to this function
2250+
2251+
@param bbox_format: signifies what bounding box format was used in `bboxes`. Must
2252+
specify `bbox_format` if `bboxes` is provided. Supported bbox_format values
2253+
are "pascal_voc", "pascal_voc_norm", "coco", and "yolo"
2254+
2255+
@returns: Augmented PIL Image
2256+
"""
2257+
return F.split_and_shuffle(
2258+
image,
2259+
n_columns=self.n_columns,
2260+
n_rows=self.n_rows,
2261+
seed=self.seed,
2262+
metadata=metadata,
2263+
bboxes=bboxes,
2264+
bbox_format=bbox_format,
2265+
)
2266+
2267+
22102268
class VFlip(BaseTransform):
22112269
def apply_transform(
22122270
self,

augly/tests/assets/expected_metadata/image_tests/expected_metadata.json

+17
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,23 @@
741741
"src_width": 1920
742742
}
743743
],
744+
"split_and_shuffle": [
745+
{
746+
"bbox_format": "yolo",
747+
"dst_bboxes": [[0.5, 0.5, 0.25, 0.75]],
748+
"dst_height": 1080,
749+
"dst_width": 1920,
750+
"intensity": 91.66666666666666,
751+
"name": "split_and_shuffle",
752+
"n_columns": 3,
753+
"n_rows": 4,
754+
"output_path": null,
755+
"seed": 10,
756+
"src_bboxes": [[0.5, 0.5, 0.25, 0.75]],
757+
"src_height": 1080,
758+
"src_width": 1920
759+
}
760+
],
744761
"vflip": [
745762
{
746763
"bbox_format": "yolo",
Loading

augly/tests/image_tests/functional_unit_test.py

+3
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ def test_shuffle_pixels(self):
138138
def test_skew(self):
139139
self.evaluate_function(imaugs.skew)
140140

141+
def test_split_and_shuffle(self):
142+
self.evaluate_function(imaugs.split_and_shuffle, n_columns=3, n_rows=4)
143+
141144
def test_vflip(self):
142145
self.evaluate_function(imaugs.vflip)
143146

augly/tests/image_tests/transforms_unit_test.py

+5
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ def test_ShufflePixels(self):
220220
def test_Skew(self):
221221
self.evaluate_class(imaugs.Skew(), fname="skew")
222222

223+
def test_SplitAndShuffle(self):
224+
self.evaluate_class(
225+
imaugs.SplitAndShuffle(n_columns=3, n_rows=4), fname="split_and_shuffle"
226+
)
227+
223228
def test_VFlip(self):
224229
self.evaluate_class(imaugs.VFlip(), fname="vflip")
225230

0 commit comments

Comments
 (0)