Commit d660903c authored by Sam Tsai's avatar Sam Tsai Committed by Facebook GitHub Bot
Browse files

use nearest interpolation when applying segmentation warp for affine augmentation

Summary:
Pull Request resolved: https://github.com/facebookresearch/d2go/pull/413

Switch to using nearest pixel interpolation when warping and added unit test.

Reviewed By: wat3rBro

Differential Revision: D41042506

fbshipit-source-id: 92b817f21e862422428a0d0df969ec9e037f99fb
parent 69dac5b5
#!/usr/bin/env python3 #!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import copy
import json import json
import random import random
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
...@@ -37,18 +37,29 @@ class AffineTransform(Transform): ...@@ -37,18 +37,29 @@ class AffineTransform(Transform):
if border_mode is not None: if border_mode is not None:
self.warp_kwargs["borderMode"] = border_mode self.warp_kwargs["borderMode"] = border_mode
def apply_image(self, img: np.ndarray) -> np.ndarray: def _warp_array(self, input_data: np.array, interp_flag: Optional[int] = None):
warp_kwargs = copy.deepcopy(self.warp_kwargs)
if interp_flag is not None:
flags = warp_kwargs.get("flags", 0)
# remove previous interp and add the new one
flags = (flags - (flags & cv2.INTER_MAX)) + interp_flag
warp_kwargs["flags"] = flags
M = self.M M = self.M
if self.is_inversed_M: if self.is_inversed_M:
M = M[:2] M = M[:2]
img = cv2.warpAffine( img = cv2.warpAffine(
img, input_data,
M, M,
(int(self.img_w), (self.img_h)), (int(self.img_w), (self.img_h)),
**self.warp_kwargs, **warp_kwargs,
) )
return img return img
def apply_image(self, img: np.ndarray) -> np.ndarray:
return self._warp_array(img)
def apply_coords(self, coords: np.ndarray) -> np.ndarray: def apply_coords(self, coords: np.ndarray) -> np.ndarray:
# Add row of ones to enable matrix multiplication # Add row of ones to enable matrix multiplication
coords = coords.T coords = coords.T
...@@ -61,7 +72,7 @@ class AffineTransform(Transform): ...@@ -61,7 +72,7 @@ class AffineTransform(Transform):
return coords return coords
def apply_segmentation(self, img: np.ndarray) -> np.ndarray: def apply_segmentation(self, img: np.ndarray) -> np.ndarray:
raise NotImplementedError() return self._warp_array(img, interp_flag=cv2.INTER_NEAREST)
class RandomPivotScaling(TransformGen): class RandomPivotScaling(TransformGen):
......
...@@ -86,9 +86,25 @@ def generate_test_data( ...@@ -86,9 +86,25 @@ def generate_test_data(
borderMode=cv2.BORDER_REPLICATE, borderMode=cv2.BORDER_REPLICATE,
) )
# Create test boxes # Create annotations
M_inv = np.vstack([M_inv, [0.0, 0.0, 1.0]])
test_bbox = [0.25 * img_w, 0.25 * img_h, 0.75 * img_h, 0.75 * img_h] test_bbox = [0.25 * img_w, 0.25 * img_h, 0.75 * img_h, 0.75 * img_h]
# Generate segmentation test data
segm_mask = np.zeros_like(source_img)
segm_mask[
int(test_bbox[0]) : int(test_bbox[2]), int(test_bbox[1]) : int(test_bbox[3])
] = 255
exp_out_segm = cv2.warpAffine(
segm_mask,
M_inv,
(out_w, out_h),
flags=cv2.WARP_INVERSE_MAP + cv2.INTER_NEAREST,
borderMode=cv2.BORDER_REPLICATE,
)
# Generate bounding box test data
M_inv = np.vstack([M_inv, [0.0, 0.0, 1.0]])
points = np.array( points = np.array(
[ [
[test_bbox[0], test_bbox[0], test_bbox[2], test_bbox[2]], [test_bbox[0], test_bbox[0], test_bbox[2], test_bbox[2]],
...@@ -97,7 +113,12 @@ def generate_test_data( ...@@ -97,7 +113,12 @@ def generate_test_data(
).T ).T
_xp = warp_points(points, M_inv) _xp = warp_points(points, M_inv)
out_bbox = [min(_xp[:, 0]), min(_xp[:, 1]), max(_xp[:, 0]), max(_xp[:, 1])] out_bbox = [min(_xp[:, 0]), min(_xp[:, 1]), max(_xp[:, 0]), max(_xp[:, 1])]
return aug_str, AugInput(source_img, boxes=[test_bbox]), (exp_out_img, [out_bbox])
return (
aug_str,
AugInput(source_img, boxes=[test_bbox], sem_seg=segm_mask),
(exp_out_img, [out_bbox], exp_out_segm),
)
def warp_points(coords: np.array, xfm_M: np.array): def warp_points(coords: np.array, xfm_M: np.array):
...@@ -110,17 +131,25 @@ def warp_points(coords: np.array, xfm_M: np.array): ...@@ -110,17 +131,25 @@ def warp_points(coords: np.array, xfm_M: np.array):
class TestDataTransformsAffine(unittest.TestCase): class TestDataTransformsAffine(unittest.TestCase):
def _check_array_close(self, aug_output, exp_img, exp_bboxes): def _validate_results(self, aug_output, exp_outputs):
exp_img = exp_outputs[0]
self.assertTrue( self.assertTrue(
np.allclose(exp_img, aug_output.image), np.allclose(exp_img, aug_output.image),
f"Augmented image not the same, expecting\n{exp_img[:,:,0]} \n got\n{aug_output.image[:,:,0]} ", f"Augmented image not the same, expecting\n{exp_img[:,:,0]} \n got\n{aug_output.image[:,:,0]} ",
) )
exp_bboxes = exp_outputs[1]
self.assertTrue( self.assertTrue(
np.allclose(exp_bboxes, aug_output.boxes, atol=0.000001), np.allclose(exp_bboxes, aug_output.boxes, atol=0.000001),
f"Augmented bbox not the same, expecting\n{exp_img[:,:,0]} \n got\n{aug_output.image[:,:,0]} ", f"Augmented bbox not the same, expecting\n{exp_img[:,:,0]} \n got\n{aug_output.image[:,:,0]} ",
) )
exp_segm = exp_outputs[2]
self.assertTrue(
np.allclose(exp_segm, aug_output.sem_seg),
f"Augmented segm not the same, expecting\n{exp_segm} \n got\n{aug_output.sem_seg[:,:]} ",
)
def test_affine_transforms_angle(self): def test_affine_transforms_angle(self):
default_cfg = Detectron2GoRunner.get_default_cfg() default_cfg = Detectron2GoRunner.get_default_cfg()
...@@ -129,15 +158,13 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -129,15 +158,13 @@ class TestDataTransformsAffine(unittest.TestCase):
img[((img_sz + 1) // 2) - 1, :, :] = 255 img[((img_sz + 1) // 2) - 1, :, :] = 255
for angle in [45, 90]: for angle in [45, 90]:
aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data( aug_str, aug_input, exp_outputs = generate_test_data(img, angle=angle)
img, angle=angle
)
default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str] default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
tfm = build_transform_gen(default_cfg, is_train=True) tfm = build_transform_gen(default_cfg, is_train=True)
# Test augmentation # Test augmentation
aug_output, _ = apply_augmentations(tfm, aug_input) aug_output, _ = apply_augmentations(tfm, aug_input)
self._check_array_close(aug_output, exp_out_img, exp_out_bboxes) self._validate_results(aug_output, exp_outputs)
def test_affine_transforms_translation(self): def test_affine_transforms_translation(self):
default_cfg = Detectron2GoRunner.get_default_cfg() default_cfg = Detectron2GoRunner.get_default_cfg()
...@@ -148,7 +175,7 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -148,7 +175,7 @@ class TestDataTransformsAffine(unittest.TestCase):
for translation in [0, 1, 2]: for translation in [0, 1, 2]:
# Test image # Test image
aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data( aug_str, aug_input, exp_outputs = generate_test_data(
img, translation=translation img, translation=translation
) )
default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str] default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
...@@ -156,7 +183,7 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -156,7 +183,7 @@ class TestDataTransformsAffine(unittest.TestCase):
# Test augmentation # Test augmentation
aug_output, _ = apply_augmentations(tfm, aug_input) aug_output, _ = apply_augmentations(tfm, aug_input)
self._check_array_close(aug_output, exp_out_img, exp_out_bboxes) self._validate_results(aug_output, exp_outputs)
def test_affine_transforms_shear(self): def test_affine_transforms_shear(self):
default_cfg = Detectron2GoRunner.get_default_cfg() default_cfg = Detectron2GoRunner.get_default_cfg()
...@@ -166,15 +193,13 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -166,15 +193,13 @@ class TestDataTransformsAffine(unittest.TestCase):
img[((img_sz + 1) // 2) - 1, :, :] = 255 img[((img_sz + 1) // 2) - 1, :, :] = 255
for shear in [0, 1, 2]: for shear in [0, 1, 2]:
aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data( aug_str, aug_input, exp_outputs = generate_test_data(img, shear=shear)
img, shear=shear
)
default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str] default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
tfm = build_transform_gen(default_cfg, is_train=True) tfm = build_transform_gen(default_cfg, is_train=True)
# Test augmentation # Test augmentation
aug_output, _ = apply_augmentations(tfm, aug_input) aug_output, _ = apply_augmentations(tfm, aug_input)
self._check_array_close(aug_output, exp_out_img, exp_out_bboxes) self._validate_results(aug_output, exp_outputs)
def test_affine_transforms_scale(self): def test_affine_transforms_scale(self):
default_cfg = Detectron2GoRunner.get_default_cfg() default_cfg = Detectron2GoRunner.get_default_cfg()
...@@ -184,15 +209,13 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -184,15 +209,13 @@ class TestDataTransformsAffine(unittest.TestCase):
img[((img_sz + 1) // 2) - 1, :, :] = 255 img[((img_sz + 1) // 2) - 1, :, :] = 255
for scale in [0.9, 1, 1.1]: for scale in [0.9, 1, 1.1]:
aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data( aug_str, aug_input, exp_outputs = generate_test_data(img, scale=scale)
img, scale=scale
)
default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str] default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
tfm = build_transform_gen(default_cfg, is_train=True) tfm = build_transform_gen(default_cfg, is_train=True)
# Test augmentation # Test augmentation
aug_output, _ = apply_augmentations(tfm, aug_input) aug_output, _ = apply_augmentations(tfm, aug_input)
self._check_array_close(aug_output, exp_out_img, exp_out_bboxes) self._validate_results(aug_output, exp_outputs)
def test_affine_transforms_angle_non_square(self): def test_affine_transforms_angle_non_square(self):
default_cfg = Detectron2GoRunner.get_default_cfg() default_cfg = Detectron2GoRunner.get_default_cfg()
...@@ -202,7 +225,7 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -202,7 +225,7 @@ class TestDataTransformsAffine(unittest.TestCase):
img[((img_sz + 1) // 2) - 1, :, :] = 255 img[((img_sz + 1) // 2) - 1, :, :] = 255
for keep_aspect_ratio in [False, True]: for keep_aspect_ratio in [False, True]:
aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data( aug_str, aug_input, exp_outputs = generate_test_data(
img, angle=45, keep_aspect_ratio=keep_aspect_ratio img, angle=45, keep_aspect_ratio=keep_aspect_ratio
) )
default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str] default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
...@@ -210,7 +233,7 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -210,7 +233,7 @@ class TestDataTransformsAffine(unittest.TestCase):
# Test augmentation # Test augmentation
aug_output, _ = apply_augmentations(tfm, aug_input) aug_output, _ = apply_augmentations(tfm, aug_input)
self._check_array_close(aug_output, exp_out_img, exp_out_bboxes) self._validate_results(aug_output, exp_outputs)
def test_affine_transforms_angle_no_fit_to_frame(self): def test_affine_transforms_angle_no_fit_to_frame(self):
default_cfg = Detectron2GoRunner.get_default_cfg() default_cfg = Detectron2GoRunner.get_default_cfg()
...@@ -219,7 +242,7 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -219,7 +242,7 @@ class TestDataTransformsAffine(unittest.TestCase):
img = np.zeros((img_sz, img_sz, 3)).astype(np.uint8) img = np.zeros((img_sz, img_sz, 3)).astype(np.uint8)
img[((img_sz + 1) // 2) - 1, :, :] = 255 img[((img_sz + 1) // 2) - 1, :, :] = 255
aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data( aug_str, aug_input, exp_outputs = generate_test_data(
img, angle=45, fit_in_frame=False img, angle=45, fit_in_frame=False
) )
default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str] default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
...@@ -227,4 +250,4 @@ class TestDataTransformsAffine(unittest.TestCase): ...@@ -227,4 +250,4 @@ class TestDataTransformsAffine(unittest.TestCase):
# Test augmentation # Test augmentation
aug_output, _ = apply_augmentations(tfm, aug_input) aug_output, _ = apply_augmentations(tfm, aug_input)
self._check_array_close(aug_output, exp_out_img, exp_out_bboxes) self._validate_results(aug_output, exp_outputs)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment