test_data_transforms_affine.py 8.68 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved

import json
import unittest
from typing import Tuple

import cv2
import numpy as np
import torchvision.transforms as T
from d2go.data.transforms.build import build_transform_gen
from d2go.runner import Detectron2GoRunner
13
from detectron2.data.transforms import apply_augmentations, AugInput
14
15
16
17
18
19
20
21


def generate_test_data(
    source_img: np.ndarray,
    angle: float = 0,
    translation: float = 0,
    scale: float = 1,
    shear: float = 0,
22
23
    fit_in_frame: bool = True,
    keep_aspect_ratio: bool = False,
24
25
26
27
28
29
30
31
) -> Tuple[str, np.ndarray]:
    # Augmentation dictionary
    aug_dict = {
        "prob": 1.0,
        "angle_range": [angle, angle],
        "translation_range": [translation, translation],
        "scale_range": [scale, scale],
        "shear_range": [shear, shear],
32
33
        "keep_aspect_ratio": keep_aspect_ratio,
        "fit_in_frame": fit_in_frame,
34
35
36
37
    }
    aug_str = "RandomAffineOp::" + json.dumps(aug_dict)

    # Get image info
38
39
    img_h, img_w = source_img.shape[0:2]
    center = [img_w / 2, img_h / 2]
40

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
    # Compute output_size
    max_size = max(img_w, img_h)
    out_w, out_h = (img_w, img_h) if keep_aspect_ratio else (max_size, max_size)

    if fit_in_frame:
        # Warp once to figure scale adjustment
        M_inv = T.functional._get_inverse_affine_matrix(
            center, angle, [0, 0], 1, [shear, shear]
        )
        M_inv.extend([0.0, 0.0, 1.0])
        M_inv = np.array(M_inv).reshape((3, 3))
        M = np.linalg.inv(M_inv)

        # Center in output patch
        img_corners = np.array(
            [
                [0, 0, img_w - 1, img_w - 1],
                [0, img_h - 1, 0, img_h - 1],
                [1, 1, 1, 1],
            ]
        )
        new_corners = M @ img_corners
        x_range = np.ceil(np.amax(new_corners[0]) - np.amin(new_corners[0]))
        y_range = np.ceil(np.amax(new_corners[1]) - np.amin(new_corners[1]))

        # Apply translation and scale after centering in output patch
        scale_adjustment = min(out_w / x_range, out_h / y_range)
        scale *= scale_adjustment

    # Adjust output center location
    translation_t = [translation, translation]
    translation_adjustment = [(out_w - img_w) / 2, (out_h - img_h) / 2]
    translation_t[0] += translation_adjustment[0]
    translation_t[1] += translation_adjustment[1]
75
76
77

    # Test data output generation
    M_inv = T.functional._get_inverse_affine_matrix(
78
        center, angle, translation_t, scale, [shear, shear]
79
80
81
82
83
    )
    M_inv = np.array(M_inv).reshape((2, 3))
    exp_out_img = cv2.warpAffine(
        source_img,
        M_inv,
84
        (out_w, out_h),
85
86
87
88
        flags=cv2.WARP_INVERSE_MAP + cv2.INTER_LINEAR,
        borderMode=cv2.BORDER_REPLICATE,
    )

89
    # Create annotations
90
    test_bbox = [0.25 * img_w, 0.25 * img_h, 0.75 * img_h, 0.75 * img_h]
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

    # 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]])
108
109
110
111
112
113
114
115
    points = np.array(
        [
            [test_bbox[0], test_bbox[0], test_bbox[2], test_bbox[2]],
            [test_bbox[1], test_bbox[3], test_bbox[1], test_bbox[3]],
        ]
    ).T
    _xp = warp_points(points, M_inv)
    out_bbox = [min(_xp[:, 0]), min(_xp[:, 1]), max(_xp[:, 0]), max(_xp[:, 1])]
116
117
118
119
120
121

    return (
        aug_str,
        AugInput(source_img, boxes=[test_bbox], sem_seg=segm_mask),
        (exp_out_img, [out_bbox], exp_out_segm),
    )
122
123
124
125
126
127
128
129
130


def warp_points(coords: np.array, xfm_M: np.array):
    coords = coords.T
    ones = np.ones((1, coords.shape[1]))
    coords = np.vstack((coords, ones))
    M = np.linalg.inv(xfm_M)
    coords = (M @ coords)[:2, :].T
    return coords
131
132
133


class TestDataTransformsAffine(unittest.TestCase):
134
135
    def _validate_results(self, aug_output, exp_outputs):
        exp_img = exp_outputs[0]
136
137
138
139
140
        self.assertTrue(
            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]} ",
        )

141
        exp_bboxes = exp_outputs[1]
142
        self.assertTrue(
143
144
            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]} ",
145
146
        )

147
148
149
150
151
152
        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[:,:]} ",
        )

153
    def test_affine_transforms_angle(self):
154
        default_cfg = Detectron2GoRunner.get_default_cfg()
155
156
157
158
159
160

        img_sz = 11
        img = np.zeros((img_sz, img_sz, 3)).astype(np.uint8)
        img[((img_sz + 1) // 2) - 1, :, :] = 255

        for angle in [45, 90]:
161
            aug_str, aug_input, exp_outputs = generate_test_data(img, angle=angle)
162
163
164
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

165
166
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
167
            self._validate_results(aug_output, exp_outputs)
168
169

    def test_affine_transforms_translation(self):
170
        default_cfg = Detectron2GoRunner.get_default_cfg()
171
172
173
174
175
176

        img_sz = 11
        img = np.zeros((img_sz, img_sz, 3)).astype(np.uint8)
        img[((img_sz + 1) // 2) - 1, :, :] = 255

        for translation in [0, 1, 2]:
177
            # Test image
178
            aug_str, aug_input, exp_outputs = generate_test_data(
179
180
                img, translation=translation
            )
181
182
183
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

184
185
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
186
            self._validate_results(aug_output, exp_outputs)
187
188

    def test_affine_transforms_shear(self):
189
        default_cfg = Detectron2GoRunner.get_default_cfg()
190
191
192
193
194
195

        img_sz = 11
        img = np.zeros((img_sz, img_sz, 3)).astype(np.uint8)
        img[((img_sz + 1) // 2) - 1, :, :] = 255

        for shear in [0, 1, 2]:
196
            aug_str, aug_input, exp_outputs = generate_test_data(img, shear=shear)
197
198
199
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

200
201
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
202
            self._validate_results(aug_output, exp_outputs)
203
204

    def test_affine_transforms_scale(self):
205
        default_cfg = Detectron2GoRunner.get_default_cfg()
206
207
208
209
210
211

        img_sz = 11
        img = np.zeros((img_sz, img_sz, 3)).astype(np.uint8)
        img[((img_sz + 1) // 2) - 1, :, :] = 255

        for scale in [0.9, 1, 1.1]:
212
            aug_str, aug_input, exp_outputs = generate_test_data(img, scale=scale)
213
214
215
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

216
217
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
218
            self._validate_results(aug_output, exp_outputs)
219
220
221
222
223
224
225
226
227

    def test_affine_transforms_angle_non_square(self):
        default_cfg = Detectron2GoRunner.get_default_cfg()

        img_sz = 11
        img = np.zeros((img_sz, img_sz - 2, 3)).astype(np.uint8)
        img[((img_sz + 1) // 2) - 1, :, :] = 255

        for keep_aspect_ratio in [False, True]:
228
            aug_str, aug_input, exp_outputs = generate_test_data(
229
230
231
232
233
                img, angle=45, keep_aspect_ratio=keep_aspect_ratio
            )
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

234
235
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
236
            self._validate_results(aug_output, exp_outputs)
237
238
239
240
241
242
243
244

    def test_affine_transforms_angle_no_fit_to_frame(self):
        default_cfg = Detectron2GoRunner.get_default_cfg()

        img_sz = 11
        img = np.zeros((img_sz, img_sz, 3)).astype(np.uint8)
        img[((img_sz + 1) // 2) - 1, :, :] = 255

245
        aug_str, aug_input, exp_outputs = generate_test_data(
246
247
            img, angle=45, fit_in_frame=False
        )
248
249
250
        default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
        tfm = build_transform_gen(default_cfg, is_train=True)

251
252
        # Test augmentation
        aug_output, _ = apply_augmentations(tfm, aug_input)
253
        self._validate_results(aug_output, exp_outputs)