test_data_transforms_affine.py 8.2 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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
    # Create test boxes
    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]
    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])]
    return aug_str, AugInput(source_img, boxes=[test_bbox]), (exp_out_img, [out_bbox])


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
110
111
112


class TestDataTransformsAffine(unittest.TestCase):
113
114
115
116
117
118
    def _check_array_close(self, aug_output, exp_img, exp_bboxes):
        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]} ",
        )

119
        self.assertTrue(
120
121
            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]} ",
122
123
124
        )

    def test_affine_transforms_angle(self):
125
        default_cfg = Detectron2GoRunner.get_default_cfg()
126
127
128
129
130
131

        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]:
132
133
134
            aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data(
                img, angle=angle
            )
135
136
137
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

138
139
140
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
            self._check_array_close(aug_output, exp_out_img, exp_out_bboxes)
141
142

    def test_affine_transforms_translation(self):
143
        default_cfg = Detectron2GoRunner.get_default_cfg()
144
145
146
147
148
149

        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]:
150
151
152
153
            # Test image
            aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data(
                img, translation=translation
            )
154
155
156
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

157
158
159
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
            self._check_array_close(aug_output, exp_out_img, exp_out_bboxes)
160
161

    def test_affine_transforms_shear(self):
162
        default_cfg = Detectron2GoRunner.get_default_cfg()
163
164
165
166
167
168

        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]:
169
170
171
            aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data(
                img, shear=shear
            )
172
173
174
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

175
176
177
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
            self._check_array_close(aug_output, exp_out_img, exp_out_bboxes)
178
179

    def test_affine_transforms_scale(self):
180
        default_cfg = Detectron2GoRunner.get_default_cfg()
181
182
183
184
185
186

        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]:
187
188
189
            aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data(
                img, scale=scale
            )
190
191
192
            default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
            tfm = build_transform_gen(default_cfg, is_train=True)

193
194
195
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
            self._check_array_close(aug_output, exp_out_img, exp_out_bboxes)
196
197
198
199
200
201
202
203
204

    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]:
205
            aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data(
206
207
208
209
210
                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)

211
212
213
            # Test augmentation
            aug_output, _ = apply_augmentations(tfm, aug_input)
            self._check_array_close(aug_output, exp_out_img, exp_out_bboxes)
214
215
216
217
218
219
220
221

    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

222
223
224
        aug_str, aug_input, (exp_out_img, exp_out_bboxes) = generate_test_data(
            img, angle=45, fit_in_frame=False
        )
225
226
227
        default_cfg.D2GO_DATA.AUG_OPS.TRAIN = [aug_str]
        tfm = build_transform_gen(default_cfg, is_train=True)

228
229
230
        # Test augmentation
        aug_output, _ = apply_augmentations(tfm, aug_input)
        self._check_array_close(aug_output, exp_out_img, exp_out_bboxes)