Add files via upload

main
RE-OWOD 2022-01-05 14:35:20 +08:00 committed by GitHub
parent 340d0b4b00
commit 1e0f9f03b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 3447 additions and 0 deletions

9
tests/README.md 100644
View File

@ -0,0 +1,9 @@
## Unit Tests
To run the unittests, do:
```
cd detectron2
python -m unittest discover -v -s ./tests
```
There are also end-to-end inference & training tests, in [dev/run_*_tests.sh](../dev).

View File

@ -0,0 +1 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved

View File

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import contextlib
import io
import numpy as np
import unittest
from collections import defaultdict
import torch
import tqdm
from fvcore.common.benchmark import benchmark
from fvcore.common.file_io import PathManager
from pycocotools.coco import COCO
from tabulate import tabulate
from torch.nn import functional as F
from detectron2.data import MetadataCatalog
from detectron2.layers.mask_ops import (
pad_masks,
paste_mask_in_image_old,
paste_masks_in_image,
scale_boxes,
)
from detectron2.structures import BitMasks, Boxes, BoxMode, PolygonMasks
from detectron2.structures.masks import polygons_to_bitmask
def iou_between_full_image_bit_masks(a, b):
intersect = (a & b).sum()
union = (a | b).sum()
return intersect / union
def rasterize_polygons_with_grid_sample(full_image_bit_mask, box, mask_size, threshold=0.5):
x0, y0, x1, y1 = box[0], box[1], box[2], box[3]
img_h, img_w = full_image_bit_mask.shape
mask_y = np.arange(0.0, mask_size) + 0.5 # mask y sample coords in [0.5, mask_size - 0.5]
mask_x = np.arange(0.0, mask_size) + 0.5 # mask x sample coords in [0.5, mask_size - 0.5]
mask_y = mask_y / mask_size * (y1 - y0) + y0
mask_x = mask_x / mask_size * (x1 - x0) + x0
mask_x = (mask_x - 0.5) / (img_w - 1) * 2 + -1
mask_y = (mask_y - 0.5) / (img_h - 1) * 2 + -1
gy, gx = torch.meshgrid(torch.from_numpy(mask_y), torch.from_numpy(mask_x))
ind = torch.stack([gx, gy], dim=-1).to(dtype=torch.float32)
full_image_bit_mask = torch.from_numpy(full_image_bit_mask)
mask = F.grid_sample(
full_image_bit_mask[None, None, :, :].to(dtype=torch.float32),
ind[None, :, :, :],
align_corners=True,
)
return mask[0, 0] >= threshold
class TestMaskCropPaste(unittest.TestCase):
def setUp(self):
json_file = MetadataCatalog.get("coco_2017_val_100").json_file
if not PathManager.isfile(json_file):
raise unittest.SkipTest("{} not found".format(json_file))
with contextlib.redirect_stdout(io.StringIO()):
json_file = PathManager.get_local_path(json_file)
self.coco = COCO(json_file)
def test_crop_paste_consistency(self):
"""
rasterize_polygons_within_box (used in training)
and
paste_masks_in_image (used in inference)
should be inverse operations to each other.
This function runs several implementation of the above two operations and prints
the reconstruction error.
"""
anns = self.coco.loadAnns(self.coco.getAnnIds(iscrowd=False)) # avoid crowd annotations
selected_anns = anns[:100]
ious = []
for ann in tqdm.tqdm(selected_anns):
results = self.process_annotation(ann)
ious.append([k[2] for k in results])
ious = np.array(ious)
mean_ious = ious.mean(axis=0)
table = []
res_dic = defaultdict(dict)
for row, iou in zip(results, mean_ious):
table.append((row[0], row[1], iou))
res_dic[row[0]][row[1]] = iou
print(tabulate(table, headers=["rasterize", "paste", "iou"], tablefmt="simple"))
# assert that the reconstruction is good:
self.assertTrue(res_dic["polygon"]["aligned"] > 0.94)
self.assertTrue(res_dic["roialign"]["aligned"] > 0.95)
def process_annotation(self, ann, mask_side_len=28):
# Parse annotation data
img_info = self.coco.loadImgs(ids=[ann["image_id"]])[0]
height, width = img_info["height"], img_info["width"]
gt_polygons = [np.array(p, dtype=np.float64) for p in ann["segmentation"]]
gt_bbox = BoxMode.convert(ann["bbox"], BoxMode.XYWH_ABS, BoxMode.XYXY_ABS)
gt_bit_mask = polygons_to_bitmask(gt_polygons, height, width)
# Run rasterize ..
torch_gt_bbox = torch.tensor(gt_bbox).to(dtype=torch.float32).reshape(-1, 4)
box_bitmasks = {
"polygon": PolygonMasks([gt_polygons]).crop_and_resize(torch_gt_bbox, mask_side_len)[0],
"gridsample": rasterize_polygons_with_grid_sample(gt_bit_mask, gt_bbox, mask_side_len),
"roialign": BitMasks(torch.from_numpy(gt_bit_mask[None, :, :])).crop_and_resize(
torch_gt_bbox, mask_side_len
)[0],
}
# Run paste ..
results = defaultdict(dict)
for k, box_bitmask in box_bitmasks.items():
padded_bitmask, scale = pad_masks(box_bitmask[None, :, :], 1)
scaled_boxes = scale_boxes(torch_gt_bbox, scale)
r = results[k]
r["old"] = paste_mask_in_image_old(
padded_bitmask[0], scaled_boxes[0], height, width, threshold=0.5
)
r["aligned"] = paste_masks_in_image(
box_bitmask[None, :, :], Boxes(torch_gt_bbox), (height, width)
)[0]
table = []
for rasterize_method, r in results.items():
for paste_method, mask in r.items():
mask = np.asarray(mask)
iou = iou_between_full_image_bit_masks(gt_bit_mask.astype("uint8"), mask)
table.append((rasterize_method, paste_method, iou))
return table
def test_polygon_area(self):
# Draw polygon boxes
for d in [5.0, 10.0, 1000.0]:
polygon = PolygonMasks([[[0, 0, 0, d, d, d, d, 0]]])
area = polygon.area()[0]
target = d ** 2
self.assertEqual(area, target)
# Draw polygon triangles
for d in [5.0, 10.0, 1000.0]:
polygon = PolygonMasks([[[0, 0, 0, d, d, d]]])
area = polygon.area()[0]
target = d ** 2 / 2
self.assertEqual(area, target)
def benchmark_paste():
S = 800
H, W = image_shape = (S, S)
N = 64
torch.manual_seed(42)
masks = torch.rand(N, 28, 28)
center = torch.rand(N, 2) * 600 + 100
wh = torch.clamp(torch.randn(N, 2) * 40 + 200, min=50)
x0y0 = torch.clamp(center - wh * 0.5, min=0.0)
x1y1 = torch.clamp(center + wh * 0.5, max=S)
boxes = Boxes(torch.cat([x0y0, x1y1], axis=1))
def func(device, n=3):
m = masks.to(device=device)
b = boxes.to(device=device)
def bench():
for _ in range(n):
paste_masks_in_image(m, b, image_shape)
if device.type == "cuda":
torch.cuda.synchronize()
return bench
specs = [{"device": torch.device("cpu"), "n": 3}]
if torch.cuda.is_available():
specs.append({"device": torch.device("cuda"), "n": 3})
benchmark(func, "paste_masks", specs, num_iters=10, warmup_iters=2)
if __name__ == "__main__":
benchmark_paste()
unittest.main()

View File

@ -0,0 +1,39 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
from __future__ import absolute_import, division, print_function, unicode_literals
import unittest
import torch
from detectron2.layers import batched_nms
from detectron2.utils.env import TORCH_VERSION
class TestNMS(unittest.TestCase):
def _create_tensors(self, N):
boxes = torch.rand(N, 4) * 100
# Note: the implementation of this function in torchvision is:
# boxes[:, 2:] += torch.rand(N, 2) * 100
# but it does not guarantee non-negative widths/heights constraints:
# boxes[:, 2] >= boxes[:, 0] and boxes[:, 3] >= boxes[:, 1]:
boxes[:, 2:] += boxes[:, :2]
scores = torch.rand(N)
return boxes, scores
@unittest.skipIf(TORCH_VERSION < (1, 6), "Insufficient pytorch version")
def test_nms_scriptability(self):
N = 2000
num_classes = 50
boxes, scores = self._create_tensors(N)
idxs = torch.randint(0, num_classes, (N,))
scripted_batched_nms = torch.jit.script(batched_nms)
err_msg = "NMS is incompatible with jit-scripted NMS for IoU={}"
for iou in [0.2, 0.5, 0.8]:
keep_ref = batched_nms(boxes, scores, idxs, iou)
backup = boxes.clone()
scripted_keep = scripted_batched_nms(boxes, scores, idxs, iou)
assert torch.allclose(boxes, backup), "boxes modified by jit-scripted batched_nms"
self.assertTrue(torch.equal(keep_ref, scripted_keep), err_msg.format(iou))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,187 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
from __future__ import absolute_import, division, print_function, unicode_literals
import numpy as np
import unittest
import torch
from torchvision import ops
from detectron2.layers import batched_nms, batched_nms_rotated, nms_rotated
def nms_edit_distance(keep1, keep2):
"""
Compare the "keep" result of two nms call.
They are allowed to be different in terms of edit distance
due to floating point precision issues, e.g.,
if a box happen to have an IoU of 0.5 with another box,
one implentation may choose to keep it while another may discard it.
"""
if torch.equal(keep1, keep2):
# they should be equal most of the time
return 0
keep1, keep2 = tuple(keep1.cpu()), tuple(keep2.cpu())
m, n = len(keep1), len(keep2)
# edit distance with DP
f = [np.arange(n + 1), np.arange(n + 1)]
for i in range(m):
cur_row = i % 2
other_row = (i + 1) % 2
f[other_row][0] = i + 1
for j in range(n):
f[other_row][j + 1] = (
f[cur_row][j]
if keep1[i] == keep2[j]
else min(min(f[cur_row][j], f[cur_row][j + 1]), f[other_row][j]) + 1
)
return f[m % 2][n]
class TestNMSRotated(unittest.TestCase):
def reference_horizontal_nms(self, boxes, scores, iou_threshold):
"""
Args:
box_scores (N, 5): boxes in corner-form and probabilities.
(Note here 5 == 4 + 1, i.e., 4-dim horizontal box + 1-dim prob)
iou_threshold: intersection over union threshold.
Returns:
picked: a list of indexes of the kept boxes
"""
picked = []
_, indexes = scores.sort(descending=True)
while len(indexes) > 0:
current = indexes[0]
picked.append(current.item())
if len(indexes) == 1:
break
current_box = boxes[current, :]
indexes = indexes[1:]
rest_boxes = boxes[indexes, :]
iou = ops.box_iou(rest_boxes, current_box.unsqueeze(0)).squeeze(1)
indexes = indexes[iou <= iou_threshold]
return torch.as_tensor(picked)
def _create_tensors(self, N):
boxes = torch.rand(N, 4) * 100
# Note: the implementation of this function in torchvision is:
# boxes[:, 2:] += torch.rand(N, 2) * 100
# but it does not guarantee non-negative widths/heights constraints:
# boxes[:, 2] >= boxes[:, 0] and boxes[:, 3] >= boxes[:, 1]:
boxes[:, 2:] += boxes[:, :2]
scores = torch.rand(N)
return boxes, scores
def test_batched_nms_rotated_0_degree_cpu(self):
N = 2000
num_classes = 50
boxes, scores = self._create_tensors(N)
idxs = torch.randint(0, num_classes, (N,))
rotated_boxes = torch.zeros(N, 5)
rotated_boxes[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2.0
rotated_boxes[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2.0
rotated_boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
rotated_boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
err_msg = "Rotated NMS with 0 degree is incompatible with horizontal NMS for IoU={}"
for iou in [0.2, 0.5, 0.8]:
backup = boxes.clone()
keep_ref = batched_nms(boxes, scores, idxs, iou)
assert torch.allclose(boxes, backup), "boxes modified by batched_nms"
backup = rotated_boxes.clone()
keep = batched_nms_rotated(rotated_boxes, scores, idxs, iou)
assert torch.allclose(
rotated_boxes, backup
), "rotated_boxes modified by batched_nms_rotated"
self.assertLessEqual(nms_edit_distance(keep, keep_ref), 1, err_msg.format(iou))
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_batched_nms_rotated_0_degree_cuda(self):
N = 2000
num_classes = 50
boxes, scores = self._create_tensors(N)
idxs = torch.randint(0, num_classes, (N,))
rotated_boxes = torch.zeros(N, 5)
rotated_boxes[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2.0
rotated_boxes[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2.0
rotated_boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
rotated_boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
err_msg = "Rotated NMS with 0 degree is incompatible with horizontal NMS for IoU={}"
for iou in [0.2, 0.5, 0.8]:
backup = boxes.clone()
keep_ref = batched_nms(boxes.cuda(), scores.cuda(), idxs, iou)
self.assertTrue(torch.allclose(boxes, backup), "boxes modified by batched_nms")
backup = rotated_boxes.clone()
keep = batched_nms_rotated(rotated_boxes.cuda(), scores.cuda(), idxs, iou)
self.assertTrue(
torch.allclose(rotated_boxes, backup),
"rotated_boxes modified by batched_nms_rotated",
)
self.assertLessEqual(nms_edit_distance(keep, keep_ref), 2, err_msg.format(iou))
def test_nms_rotated_0_degree_cpu(self):
N = 1000
boxes, scores = self._create_tensors(N)
rotated_boxes = torch.zeros(N, 5)
rotated_boxes[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2.0
rotated_boxes[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2.0
rotated_boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
rotated_boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
err_msg = "Rotated NMS incompatible between CPU and reference implementation for IoU={}"
for iou in [0.5]:
keep_ref = self.reference_horizontal_nms(boxes, scores, iou)
keep = nms_rotated(rotated_boxes, scores, iou)
self.assertLessEqual(nms_edit_distance(keep, keep_ref), 1, err_msg.format(iou))
def test_nms_rotated_90_degrees_cpu(self):
N = 1000
boxes, scores = self._create_tensors(N)
rotated_boxes = torch.zeros(N, 5)
rotated_boxes[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2.0
rotated_boxes[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2.0
# Note for rotated_boxes[:, 2] and rotated_boxes[:, 3]:
# widths and heights are intentionally swapped here for 90 degrees case
# so that the reference horizontal nms could be used
rotated_boxes[:, 2] = boxes[:, 3] - boxes[:, 1]
rotated_boxes[:, 3] = boxes[:, 2] - boxes[:, 0]
rotated_boxes[:, 4] = torch.ones(N) * 90
err_msg = "Rotated NMS incompatible between CPU and reference implementation for IoU={}"
for iou in [0.2, 0.5, 0.8]:
keep_ref = self.reference_horizontal_nms(boxes, scores, iou)
keep = nms_rotated(rotated_boxes, scores, iou)
self.assertLessEqual(nms_edit_distance(keep, keep_ref), 1, err_msg.format(iou))
def test_nms_rotated_180_degrees_cpu(self):
N = 1000
boxes, scores = self._create_tensors(N)
rotated_boxes = torch.zeros(N, 5)
rotated_boxes[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2.0
rotated_boxes[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2.0
rotated_boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
rotated_boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
rotated_boxes[:, 4] = torch.ones(N) * 180
err_msg = "Rotated NMS incompatible between CPU and reference implementation for IoU={}"
for iou in [0.2, 0.5, 0.8]:
keep_ref = self.reference_horizontal_nms(boxes, scores, iou)
keep = nms_rotated(rotated_boxes, scores, iou)
self.assertLessEqual(nms_edit_distance(keep, keep_ref), 1, err_msg.format(iou))
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_nms_rotated_0_degree_cuda(self):
N = 1000
boxes, scores = self._create_tensors(N)
rotated_boxes = torch.zeros(N, 5)
rotated_boxes[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2.0
rotated_boxes[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2.0
rotated_boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
rotated_boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
err_msg = "Rotated NMS incompatible between CPU and CUDA for IoU={}"
for iou in [0.2, 0.5, 0.8]:
r_cpu = nms_rotated(rotated_boxes, scores, iou)
r_cuda = nms_rotated(rotated_boxes.cuda(), scores.cuda(), iou)
self.assertLessEqual(nms_edit_distance(r_cpu, r_cuda.cpu()), 1, err_msg.format(iou))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,152 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import numpy as np
import unittest
import cv2
import torch
from fvcore.common.benchmark import benchmark
from detectron2.layers.roi_align import ROIAlign
class ROIAlignTest(unittest.TestCase):
def test_forward_output(self):
input = np.arange(25).reshape(5, 5).astype("float32")
"""
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
"""
output = self._simple_roialign(input, [1, 1, 3, 3], (4, 4), aligned=False)
output_correct = self._simple_roialign(input, [1, 1, 3, 3], (4, 4), aligned=True)
# without correction:
old_results = [
[7.5, 8, 8.5, 9],
[10, 10.5, 11, 11.5],
[12.5, 13, 13.5, 14],
[15, 15.5, 16, 16.5],
]
# with 0.5 correction:
correct_results = [
[4.5, 5.0, 5.5, 6.0],
[7.0, 7.5, 8.0, 8.5],
[9.5, 10.0, 10.5, 11.0],
[12.0, 12.5, 13.0, 13.5],
]
# This is an upsampled version of [[6, 7], [11, 12]]
self.assertTrue(np.allclose(output.flatten(), np.asarray(old_results).flatten()))
self.assertTrue(
np.allclose(output_correct.flatten(), np.asarray(correct_results).flatten())
)
# Also see similar issues in tensorflow at
# https://github.com/tensorflow/tensorflow/issues/26278
def test_resize(self):
H, W = 30, 30
input = np.random.rand(H, W).astype("float32") * 100
box = [10, 10, 20, 20]
output = self._simple_roialign(input, box, (5, 5), aligned=True)
input2x = cv2.resize(input, (W // 2, H // 2), interpolation=cv2.INTER_LINEAR)
box2x = [x / 2 for x in box]
output2x = self._simple_roialign(input2x, box2x, (5, 5), aligned=True)
diff = np.abs(output2x - output)
self.assertTrue(diff.max() < 1e-4)
def _simple_roialign(self, img, box, resolution, aligned=True):
"""
RoiAlign with scale 1.0 and 0 sample ratio.
"""
if isinstance(resolution, int):
resolution = (resolution, resolution)
op = ROIAlign(resolution, 1.0, 0, aligned=aligned)
input = torch.from_numpy(img[None, None, :, :].astype("float32"))
rois = [0] + list(box)
rois = torch.from_numpy(np.asarray(rois)[None, :].astype("float32"))
output = op.forward(input, rois)
if torch.cuda.is_available():
output_cuda = op.forward(input.cuda(), rois.cuda()).cpu()
self.assertTrue(torch.allclose(output, output_cuda))
return output[0, 0]
def _simple_roialign_with_grad(self, img, box, resolution, device):
if isinstance(resolution, int):
resolution = (resolution, resolution)
op = ROIAlign(resolution, 1.0, 0, aligned=True)
input = torch.from_numpy(img[None, None, :, :].astype("float32"))
rois = [0] + list(box)
rois = torch.from_numpy(np.asarray(rois)[None, :].astype("float32"))
input = input.to(device=device)
rois = rois.to(device=device)
input.requires_grad = True
output = op.forward(input, rois)
return input, output
def test_empty_box(self):
img = np.random.rand(5, 5)
box = [3, 4, 5, 4]
o = self._simple_roialign(img, box, 7)
self.assertTrue(o.shape == (7, 7))
self.assertTrue((o == 0).all())
for dev in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
input, output = self._simple_roialign_with_grad(img, box, 7, torch.device(dev))
output.sum().backward()
self.assertTrue(torch.allclose(input.grad, torch.zeros_like(input)))
def test_empty_batch(self):
input = torch.zeros(0, 3, 10, 10, dtype=torch.float32)
rois = torch.zeros(0, 5, dtype=torch.float32)
op = ROIAlign((7, 7), 1.0, 0, aligned=True)
output = op.forward(input, rois)
self.assertTrue(output.shape == (0, 3, 7, 7))
def benchmark_roi_align():
from detectron2 import _C
def random_boxes(mean_box, stdev, N, maxsize):
ret = torch.rand(N, 4) * stdev + torch.tensor(mean_box, dtype=torch.float)
ret.clamp_(min=0, max=maxsize)
return ret
def func(N, C, H, W, nboxes_per_img):
input = torch.rand(N, C, H, W)
boxes = []
batch_idx = []
for k in range(N):
b = random_boxes([80, 80, 130, 130], 24, nboxes_per_img, H)
# try smaller boxes:
# b = random_boxes([100, 100, 110, 110], 4, nboxes_per_img, H)
boxes.append(b)
batch_idx.append(torch.zeros(nboxes_per_img, 1, dtype=torch.float32) + k)
boxes = torch.cat(boxes, axis=0)
batch_idx = torch.cat(batch_idx, axis=0)
boxes = torch.cat([batch_idx, boxes], axis=1)
input = input.cuda()
boxes = boxes.cuda()
def bench():
_C.roi_align_forward(input, boxes, 1.0, 7, 7, 0, True)
torch.cuda.synchronize()
return bench
args = [dict(N=2, C=512, H=256, W=256, nboxes_per_img=500)]
benchmark(func, "cuda_roialign", args, num_iters=20, warmup_iters=1)
if __name__ == "__main__":
if torch.cuda.is_available():
benchmark_roi_align()
unittest.main()

View File

@ -0,0 +1,176 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
import cv2
import torch
from torch.autograd import Variable, gradcheck
from detectron2.layers.roi_align import ROIAlign
from detectron2.layers.roi_align_rotated import ROIAlignRotated
logger = logging.getLogger(__name__)
class ROIAlignRotatedTest(unittest.TestCase):
def _box_to_rotated_box(self, box, angle):
return [
(box[0] + box[2]) / 2.0,
(box[1] + box[3]) / 2.0,
box[2] - box[0],
box[3] - box[1],
angle,
]
def _rot90(self, img, num):
num = num % 4 # note: -1 % 4 == 3
for _ in range(num):
img = img.transpose(0, 1).flip(0)
return img
def test_forward_output_0_90_180_270(self):
for i in range(4):
# i = 0, 1, 2, 3 corresponding to 0, 90, 180, 270 degrees
img = torch.arange(25, dtype=torch.float32).reshape(5, 5)
"""
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24
"""
box = [1, 1, 3, 3]
rotated_box = self._box_to_rotated_box(box=box, angle=90 * i)
result = self._simple_roi_align_rotated(img=img, box=rotated_box, resolution=(4, 4))
# Here's an explanation for 0 degree case:
# point 0 in the original input lies at [0.5, 0.5]
# (the center of bin [0, 1] x [0, 1])
# point 1 in the original input lies at [1.5, 0.5], etc.
# since the resolution is (4, 4) that divides [1, 3] x [1, 3]
# into 4 x 4 equal bins,
# the top-left bin is [1, 1.5] x [1, 1.5], and its center
# (1.25, 1.25) lies at the 3/4 position
# between point 0 and point 1, point 5 and point 6,
# point 0 and point 5, point 1 and point 6, so it can be calculated as
# 0.25*(0*0.25+1*0.75)+(5*0.25+6*0.75)*0.75 = 4.5
result_expected = torch.tensor(
[
[4.5, 5.0, 5.5, 6.0],
[7.0, 7.5, 8.0, 8.5],
[9.5, 10.0, 10.5, 11.0],
[12.0, 12.5, 13.0, 13.5],
]
)
# This is also an upsampled version of [[6, 7], [11, 12]]
# When the box is rotated by 90 degrees CCW,
# the result would be rotated by 90 degrees CW, thus it's -i here
result_expected = self._rot90(result_expected, -i)
assert torch.allclose(result, result_expected)
def test_resize(self):
H, W = 30, 30
input = torch.rand(H, W) * 100
box = [10, 10, 20, 20]
rotated_box = self._box_to_rotated_box(box, angle=0)
output = self._simple_roi_align_rotated(img=input, box=rotated_box, resolution=(5, 5))
input2x = cv2.resize(input.numpy(), (W // 2, H // 2), interpolation=cv2.INTER_LINEAR)
input2x = torch.from_numpy(input2x)
box2x = [x / 2 for x in box]
rotated_box2x = self._box_to_rotated_box(box2x, angle=0)
output2x = self._simple_roi_align_rotated(img=input2x, box=rotated_box2x, resolution=(5, 5))
assert torch.allclose(output2x, output)
def _simple_roi_align_rotated(self, img, box, resolution):
"""
RoiAlignRotated with scale 1.0 and 0 sample ratio.
"""
op = ROIAlignRotated(output_size=resolution, spatial_scale=1.0, sampling_ratio=0)
input = img[None, None, :, :]
rois = [0] + list(box)
rois = torch.tensor(rois, dtype=torch.float32)[None, :]
result_cpu = op.forward(input, rois)
if torch.cuda.is_available():
result_cuda = op.forward(input.cuda(), rois.cuda())
assert torch.allclose(result_cpu, result_cuda.cpu())
return result_cpu[0, 0]
def test_empty_box(self):
img = torch.rand(5, 5)
out = self._simple_roi_align_rotated(img, [2, 3, 0, 0, 0], (7, 7))
self.assertTrue((out == 0).all())
def test_roi_align_rotated_gradcheck_cpu(self):
dtype = torch.float64
device = torch.device("cpu")
roi_align_rotated_op = ROIAlignRotated(
output_size=(5, 5), spatial_scale=0.5, sampling_ratio=1
).to(dtype=dtype, device=device)
x = torch.rand(1, 1, 10, 10, dtype=dtype, device=device, requires_grad=True)
# roi format is (batch index, x_center, y_center, width, height, angle)
rois = torch.tensor(
[[0, 4.5, 4.5, 9, 9, 0], [0, 2, 7, 4, 4, 0], [0, 7, 7, 4, 4, 0]],
dtype=dtype,
device=device,
)
def func(input):
return roi_align_rotated_op(input, rois)
assert gradcheck(func, (x,)), "gradcheck failed for RoIAlignRotated CPU"
assert gradcheck(func, (x.transpose(2, 3),)), "gradcheck failed for RoIAlignRotated CPU"
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_roi_align_rotated_gradient_cuda(self):
"""
Compute gradients for ROIAlignRotated with multiple bounding boxes on the GPU,
and compare the result with ROIAlign
"""
# torch.manual_seed(123)
dtype = torch.float64
device = torch.device("cuda")
pool_h, pool_w = (5, 5)
roi_align = ROIAlign(output_size=(pool_h, pool_w), spatial_scale=1, sampling_ratio=2).to(
device=device
)
roi_align_rotated = ROIAlignRotated(
output_size=(pool_h, pool_w), spatial_scale=1, sampling_ratio=2
).to(device=device)
x = torch.rand(1, 1, 10, 10, dtype=dtype, device=device, requires_grad=True)
# x_rotated = x.clone() won't work (will lead to grad_fun=CloneBackward)!
x_rotated = Variable(x.data.clone(), requires_grad=True)
# roi_rotated format is (batch index, x_center, y_center, width, height, angle)
rois_rotated = torch.tensor(
[[0, 4.5, 4.5, 9, 9, 0], [0, 2, 7, 4, 4, 0], [0, 7, 7, 4, 4, 0]],
dtype=dtype,
device=device,
)
y_rotated = roi_align_rotated(x_rotated, rois_rotated)
s_rotated = y_rotated.sum()
s_rotated.backward()
# roi format is (batch index, x1, y1, x2, y2)
rois = torch.tensor(
[[0, 0, 0, 9, 9], [0, 0, 5, 4, 9], [0, 5, 5, 9, 9]], dtype=dtype, device=device
)
y = roi_align(x, rois)
s = y.sum()
s.backward()
assert torch.allclose(
x.grad, x_rotated.grad
), "gradients for ROIAlign and ROIAlignRotated mismatch on CUDA"
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,122 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
import torch
from detectron2.config import get_cfg
from detectron2.layers import ShapeSpec
from detectron2.modeling.anchor_generator import DefaultAnchorGenerator, RotatedAnchorGenerator
from detectron2.utils.env import TORCH_VERSION
logger = logging.getLogger(__name__)
class TestAnchorGenerator(unittest.TestCase):
def test_default_anchor_generator(self):
cfg = get_cfg()
cfg.MODEL.ANCHOR_GENERATOR.SIZES = [[32, 64]]
cfg.MODEL.ANCHOR_GENERATOR.ASPECT_RATIOS = [[0.25, 1, 4]]
anchor_generator = DefaultAnchorGenerator(cfg, [ShapeSpec(stride=4)])
# only the last two dimensions of features matter here
num_images = 2
features = {"stage3": torch.rand(num_images, 96, 1, 2)}
anchors = anchor_generator([features["stage3"]])
expected_anchor_tensor = torch.tensor(
[
[-32.0, -8.0, 32.0, 8.0],
[-16.0, -16.0, 16.0, 16.0],
[-8.0, -32.0, 8.0, 32.0],
[-64.0, -16.0, 64.0, 16.0],
[-32.0, -32.0, 32.0, 32.0],
[-16.0, -64.0, 16.0, 64.0],
[-28.0, -8.0, 36.0, 8.0], # -28.0 == -32.0 + STRIDE (4)
[-12.0, -16.0, 20.0, 16.0],
[-4.0, -32.0, 12.0, 32.0],
[-60.0, -16.0, 68.0, 16.0],
[-28.0, -32.0, 36.0, 32.0],
[-12.0, -64.0, 20.0, 64.0],
]
)
assert torch.allclose(anchors[0].tensor, expected_anchor_tensor)
def test_default_anchor_generator_centered(self):
# test explicit args
anchor_generator = DefaultAnchorGenerator(
sizes=[32, 64], aspect_ratios=[0.25, 1, 4], strides=[4]
)
# only the last two dimensions of features matter here
num_images = 2
features = {"stage3": torch.rand(num_images, 96, 1, 2)}
expected_anchor_tensor = torch.tensor(
[
[-30.0, -6.0, 34.0, 10.0],
[-14.0, -14.0, 18.0, 18.0],
[-6.0, -30.0, 10.0, 34.0],
[-62.0, -14.0, 66.0, 18.0],
[-30.0, -30.0, 34.0, 34.0],
[-14.0, -62.0, 18.0, 66.0],
[-26.0, -6.0, 38.0, 10.0],
[-10.0, -14.0, 22.0, 18.0],
[-2.0, -30.0, 14.0, 34.0],
[-58.0, -14.0, 70.0, 18.0],
[-26.0, -30.0, 38.0, 34.0],
[-10.0, -62.0, 22.0, 66.0],
]
)
anchors = anchor_generator([features["stage3"]])
assert torch.allclose(anchors[0].tensor, expected_anchor_tensor)
if TORCH_VERSION >= (1, 6):
anchors = torch.jit.script(anchor_generator)([features["stage3"]])
assert torch.allclose(anchors[0].tensor, expected_anchor_tensor)
def test_rrpn_anchor_generator(self):
cfg = get_cfg()
cfg.MODEL.ANCHOR_GENERATOR.SIZES = [[32, 64]]
cfg.MODEL.ANCHOR_GENERATOR.ASPECT_RATIOS = [[0.25, 1, 4]]
cfg.MODEL.ANCHOR_GENERATOR.ANGLES = [0, 45] # test single list[float]
anchor_generator = RotatedAnchorGenerator(cfg, [ShapeSpec(stride=4)])
# only the last two dimensions of features matter here
num_images = 2
features = {"stage3": torch.rand(num_images, 96, 1, 2)}
anchors = anchor_generator([features["stage3"]])
expected_anchor_tensor = torch.tensor(
[
[0.0, 0.0, 64.0, 16.0, 0.0],
[0.0, 0.0, 64.0, 16.0, 45.0],
[0.0, 0.0, 32.0, 32.0, 0.0],
[0.0, 0.0, 32.0, 32.0, 45.0],
[0.0, 0.0, 16.0, 64.0, 0.0],
[0.0, 0.0, 16.0, 64.0, 45.0],
[0.0, 0.0, 128.0, 32.0, 0.0],
[0.0, 0.0, 128.0, 32.0, 45.0],
[0.0, 0.0, 64.0, 64.0, 0.0],
[0.0, 0.0, 64.0, 64.0, 45.0],
[0.0, 0.0, 32.0, 128.0, 0.0],
[0.0, 0.0, 32.0, 128.0, 45.0],
[4.0, 0.0, 64.0, 16.0, 0.0], # 4.0 == 0.0 + STRIDE (4)
[4.0, 0.0, 64.0, 16.0, 45.0],
[4.0, 0.0, 32.0, 32.0, 0.0],
[4.0, 0.0, 32.0, 32.0, 45.0],
[4.0, 0.0, 16.0, 64.0, 0.0],
[4.0, 0.0, 16.0, 64.0, 45.0],
[4.0, 0.0, 128.0, 32.0, 0.0],
[4.0, 0.0, 128.0, 32.0, 45.0],
[4.0, 0.0, 64.0, 64.0, 0.0],
[4.0, 0.0, 64.0, 64.0, 45.0],
[4.0, 0.0, 32.0, 128.0, 0.0],
[4.0, 0.0, 32.0, 128.0, 45.0],
]
)
assert torch.allclose(anchors[0].tensor, expected_anchor_tensor)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,64 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
import torch
from detectron2.modeling.box_regression import Box2BoxTransform, Box2BoxTransformRotated
logger = logging.getLogger(__name__)
def random_boxes(mean_box, stdev, N):
return torch.rand(N, 4) * stdev + torch.tensor(mean_box, dtype=torch.float)
class TestBox2BoxTransform(unittest.TestCase):
def test_reconstruction(self):
weights = (5, 5, 10, 10)
b2b_tfm = Box2BoxTransform(weights=weights)
src_boxes = random_boxes([10, 10, 20, 20], 1, 10)
dst_boxes = random_boxes([10, 10, 20, 20], 1, 10)
devices = [torch.device("cpu")]
if torch.cuda.is_available():
devices.append(torch.device("cuda"))
for device in devices:
src_boxes = src_boxes.to(device=device)
dst_boxes = dst_boxes.to(device=device)
deltas = b2b_tfm.get_deltas(src_boxes, dst_boxes)
dst_boxes_reconstructed = b2b_tfm.apply_deltas(deltas, src_boxes)
assert torch.allclose(dst_boxes, dst_boxes_reconstructed)
def random_rotated_boxes(mean_box, std_length, std_angle, N):
return torch.cat(
[torch.rand(N, 4) * std_length, torch.rand(N, 1) * std_angle], dim=1
) + torch.tensor(mean_box, dtype=torch.float)
class TestBox2BoxTransformRotated(unittest.TestCase):
def test_reconstruction(self):
weights = (5, 5, 10, 10, 1)
b2b_transform = Box2BoxTransformRotated(weights=weights)
src_boxes = random_rotated_boxes([10, 10, 20, 20, -30], 5, 60.0, 10)
dst_boxes = random_rotated_boxes([10, 10, 20, 20, -30], 5, 60.0, 10)
devices = [torch.device("cpu")]
if torch.cuda.is_available():
devices.append(torch.device("cuda"))
for device in devices:
src_boxes = src_boxes.to(device=device)
dst_boxes = dst_boxes.to(device=device)
deltas = b2b_transform.get_deltas(src_boxes, dst_boxes)
dst_boxes_reconstructed = b2b_transform.apply_deltas(deltas, src_boxes)
assert torch.allclose(dst_boxes[:, :4], dst_boxes_reconstructed[:, :4], atol=1e-5)
# angle difference has to be normalized
assert torch.allclose(
(dst_boxes[:, 4] - dst_boxes_reconstructed[:, 4] + 180.0) % 360.0 - 180.0,
torch.zeros_like(dst_boxes[:, 4]),
atol=1e-4,
)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,106 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
import torch
from detectron2.layers import ShapeSpec
from detectron2.modeling.box_regression import Box2BoxTransform, Box2BoxTransformRotated
from detectron2.modeling.roi_heads.fast_rcnn import FastRCNNOutputLayers
from detectron2.modeling.roi_heads.rotated_fast_rcnn import RotatedFastRCNNOutputLayers
from detectron2.structures import Boxes, Instances, RotatedBoxes
from detectron2.utils.events import EventStorage
logger = logging.getLogger(__name__)
class FastRCNNTest(unittest.TestCase):
def test_fast_rcnn(self):
torch.manual_seed(132)
box_head_output_size = 8
box_predictor = FastRCNNOutputLayers(
ShapeSpec(channels=box_head_output_size),
box2box_transform=Box2BoxTransform(weights=(10, 10, 5, 5)),
num_classes=5,
)
feature_pooled = torch.rand(2, box_head_output_size)
predictions = box_predictor(feature_pooled)
proposal_boxes = torch.tensor([[0.8, 1.1, 3.2, 2.8], [2.3, 2.5, 7, 8]], dtype=torch.float32)
gt_boxes = torch.tensor([[1, 1, 3, 3], [2, 2, 6, 6]], dtype=torch.float32)
proposal = Instances((10, 10))
proposal.proposal_boxes = Boxes(proposal_boxes)
proposal.gt_boxes = Boxes(gt_boxes)
proposal.gt_classes = torch.tensor([1, 2])
with EventStorage(): # capture events in a new storage to discard them
losses = box_predictor.losses(predictions, [proposal])
expected_losses = {
"loss_cls": torch.tensor(1.7951188087),
"loss_box_reg": torch.tensor(4.0357131958),
}
for name in expected_losses.keys():
assert torch.allclose(losses[name], expected_losses[name])
def test_fast_rcnn_empty_batch(self, device="cpu"):
box_predictor = FastRCNNOutputLayers(
ShapeSpec(channels=10),
box2box_transform=Box2BoxTransform(weights=(10, 10, 5, 5)),
num_classes=8,
).to(device=device)
logits = torch.randn(0, 100, requires_grad=True, device=device)
deltas = torch.randn(0, 4, requires_grad=True, device=device)
losses = box_predictor.losses([logits, deltas], [])
for value in losses.values():
self.assertTrue(torch.allclose(value, torch.zeros_like(value)))
sum(losses.values()).backward()
self.assertTrue(logits.grad is not None)
self.assertTrue(deltas.grad is not None)
predictions, _ = box_predictor.inference([logits, deltas], [])
self.assertEqual(len(predictions), 0)
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_fast_rcnn_empty_batch_cuda(self):
self.test_fast_rcnn_empty_batch(device=torch.device("cuda"))
def test_fast_rcnn_rotated(self):
torch.manual_seed(132)
box_head_output_size = 8
box_predictor = RotatedFastRCNNOutputLayers(
ShapeSpec(channels=box_head_output_size),
box2box_transform=Box2BoxTransformRotated(weights=(10, 10, 5, 5, 1)),
num_classes=5,
)
feature_pooled = torch.rand(2, box_head_output_size)
predictions = box_predictor(feature_pooled)
proposal_boxes = torch.tensor(
[[2, 1.95, 2.4, 1.7, 0], [4.65, 5.25, 4.7, 5.5, 0]], dtype=torch.float32
)
gt_boxes = torch.tensor([[2, 2, 2, 2, 0], [4, 4, 4, 4, 0]], dtype=torch.float32)
proposal = Instances((10, 10))
proposal.proposal_boxes = RotatedBoxes(proposal_boxes)
proposal.gt_boxes = RotatedBoxes(gt_boxes)
proposal.gt_classes = torch.tensor([1, 2])
with EventStorage(): # capture events in a new storage to discard them
losses = box_predictor.losses(predictions, [proposal])
# Note: the expected losses are slightly different even if
# the boxes are essentially the same as in the FastRCNNOutput test, because
# bbox_pred in FastRCNNOutputLayers have different Linear layers/initialization
# between the two cases.
expected_losses = {
"loss_cls": torch.tensor(1.7920907736),
"loss_box_reg": torch.tensor(4.0410838127),
}
for name in expected_losses.keys():
assert torch.allclose(losses[name], expected_losses[name])
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,45 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import unittest
from typing import List
import torch
from detectron2.config import get_cfg
from detectron2.modeling.matcher import Matcher
from detectron2.utils.env import TORCH_VERSION
class TestMatcher(unittest.TestCase):
# need https://github.com/pytorch/pytorch/pull/38378
@unittest.skipIf(TORCH_VERSION < (1, 6), "Insufficient pytorch version")
def test_scriptability(self):
cfg = get_cfg()
anchor_matcher = Matcher(
cfg.MODEL.RPN.IOU_THRESHOLDS, cfg.MODEL.RPN.IOU_LABELS, allow_low_quality_matches=True
)
match_quality_matrix = torch.tensor(
[[0.15, 0.45, 0.2, 0.6], [0.3, 0.65, 0.05, 0.1], [0.05, 0.4, 0.25, 0.4]]
)
expected_matches = torch.tensor([1, 1, 2, 0])
expected_match_labels = torch.tensor([-1, 1, 0, 1], dtype=torch.int8)
matches, match_labels = anchor_matcher(match_quality_matrix)
self.assertTrue(torch.allclose(matches, expected_matches))
self.assertTrue(torch.allclose(match_labels, expected_match_labels))
# nonzero_tuple must be import explicitly to let jit know what it is.
# https://github.com/pytorch/pytorch/issues/38964
from detectron2.layers import nonzero_tuple # noqa F401
def f(thresholds: List[float], labels: List[int]):
return Matcher(thresholds, labels, allow_low_quality_matches=True)
scripted_anchor_matcher = torch.jit.script(f)(
cfg.MODEL.RPN.IOU_THRESHOLDS, cfg.MODEL.RPN.IOU_LABELS
)
matches, match_labels = scripted_anchor_matcher(match_quality_matrix)
self.assertTrue(torch.allclose(matches, expected_matches))
self.assertTrue(torch.allclose(match_labels, expected_match_labels))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,157 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
import numpy as np
import unittest
import torch
import detectron2.model_zoo as model_zoo
from detectron2.config import get_cfg
from detectron2.modeling import build_model
from detectron2.structures import BitMasks, Boxes, ImageList, Instances
from detectron2.utils.events import EventStorage
def get_model_zoo(config_path):
"""
Like model_zoo.get, but do not load any weights (even pretrained)
"""
cfg_file = model_zoo.get_config_file(config_path)
cfg = get_cfg()
cfg.merge_from_file(cfg_file)
if not torch.cuda.is_available():
cfg.MODEL.DEVICE = "cpu"
return build_model(cfg)
def create_model_input(img, inst=None):
if inst is not None:
return {"image": img, "instances": inst}
else:
return {"image": img}
def get_empty_instance(h, w):
inst = Instances((h, w))
inst.gt_boxes = Boxes(torch.rand(0, 4))
inst.gt_classes = torch.tensor([]).to(dtype=torch.int64)
inst.gt_masks = BitMasks(torch.rand(0, h, w))
return inst
def get_regular_bitmask_instances(h, w):
inst = Instances((h, w))
inst.gt_boxes = Boxes(torch.rand(3, 4))
inst.gt_boxes.tensor[:, 2:] += inst.gt_boxes.tensor[:, :2]
inst.gt_classes = torch.tensor([3, 4, 5]).to(dtype=torch.int64)
inst.gt_masks = BitMasks((torch.rand(3, h, w) > 0.5))
return inst
class ModelE2ETest:
def setUp(self):
torch.manual_seed(43)
self.model = get_model_zoo(self.CONFIG_PATH)
def _test_eval(self, input_sizes):
inputs = [create_model_input(torch.rand(3, s[0], s[1])) for s in input_sizes]
self.model.eval()
self.model(inputs)
def _test_train(self, input_sizes, instances):
assert len(input_sizes) == len(instances)
inputs = [
create_model_input(torch.rand(3, s[0], s[1]), inst)
for s, inst in zip(input_sizes, instances)
]
self.model.train()
with EventStorage():
losses = self.model(inputs)
sum(losses.values()).backward()
del losses
def _inf_tensor(self, *shape):
return 1.0 / torch.zeros(*shape, device=self.model.device)
def _nan_tensor(self, *shape):
return torch.zeros(*shape, device=self.model.device).fill_(float("nan"))
def test_empty_data(self):
instances = [get_empty_instance(200, 250), get_empty_instance(200, 249)]
self._test_eval([(200, 250), (200, 249)])
self._test_train([(200, 250), (200, 249)], instances)
@unittest.skipIf(not torch.cuda.is_available(), "CUDA unavailable")
def test_eval_tocpu(self):
model = get_model_zoo(self.CONFIG_PATH).cpu()
model.eval()
input_sizes = [(200, 250), (200, 249)]
inputs = [create_model_input(torch.rand(3, s[0], s[1])) for s in input_sizes]
model(inputs)
class MaskRCNNE2ETest(ModelE2ETest, unittest.TestCase):
CONFIG_PATH = "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml"
def test_half_empty_data(self):
instances = [get_empty_instance(200, 250), get_regular_bitmask_instances(200, 249)]
self._test_train([(200, 250), (200, 249)], instances)
# This test is flaky because in some environment the output features are zero due to relu
# def test_rpn_inf_nan_data(self):
# self.model.eval()
# for tensor in [self._inf_tensor, self._nan_tensor]:
# images = ImageList(tensor(1, 3, 512, 512), [(510, 510)])
# features = {
# "p2": tensor(1, 256, 256, 256),
# "p3": tensor(1, 256, 128, 128),
# "p4": tensor(1, 256, 64, 64),
# "p5": tensor(1, 256, 32, 32),
# "p6": tensor(1, 256, 16, 16),
# }
# props, _ = self.model.proposal_generator(images, features)
# self.assertEqual(len(props[0]), 0)
def test_roiheads_inf_nan_data(self):
self.model.eval()
for tensor in [self._inf_tensor, self._nan_tensor]:
images = ImageList(tensor(1, 3, 512, 512), [(510, 510)])
features = {
"p2": tensor(1, 256, 256, 256),
"p3": tensor(1, 256, 128, 128),
"p4": tensor(1, 256, 64, 64),
"p5": tensor(1, 256, 32, 32),
"p6": tensor(1, 256, 16, 16),
}
props = [Instances((510, 510))]
props[0].proposal_boxes = Boxes([[10, 10, 20, 20]]).to(device=self.model.device)
props[0].objectness_logits = torch.tensor([1.0]).reshape(1, 1)
det, _ = self.model.roi_heads(images, features, props)
self.assertEqual(len(det[0]), 0)
class RetinaNetE2ETest(ModelE2ETest, unittest.TestCase):
CONFIG_PATH = "COCO-Detection/retinanet_R_50_FPN_1x.yaml"
def test_inf_nan_data(self):
self.model.eval()
self.model.score_threshold = -999999999
for tensor in [self._inf_tensor, self._nan_tensor]:
images = ImageList(tensor(1, 3, 512, 512), [(510, 510)])
features = [
tensor(1, 256, 128, 128),
tensor(1, 256, 64, 64),
tensor(1, 256, 32, 32),
tensor(1, 256, 16, 16),
tensor(1, 256, 8, 8),
]
anchors = self.model.anchor_generator(features)
_, pred_anchor_deltas = self.model.head(features)
HWAs = [np.prod(x.shape[-3:]) // 4 for x in pred_anchor_deltas]
pred_logits = [tensor(1, HWA, self.model.num_classes) for HWA in HWAs]
pred_anchor_deltas = [tensor(1, HWA, 4) for HWA in HWAs]
det = self.model.inference(anchors, pred_logits, pred_anchor_deltas, images.image_sizes)
# all predictions (if any) are infinite or nan
if len(det[0]):
self.assertTrue(torch.isfinite(det[0].pred_boxes.tensor).sum() == 0)

View File

@ -0,0 +1,231 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
from copy import deepcopy
import torch
from detectron2.config import get_cfg
from detectron2.export.torchscript import patch_instances
from detectron2.layers import ShapeSpec
from detectron2.modeling.proposal_generator.build import build_proposal_generator
from detectron2.modeling.roi_heads import (
FastRCNNConvFCHead,
KRCNNConvDeconvUpsampleHead,
MaskRCNNConvUpsampleHead,
StandardROIHeads,
build_roi_heads,
)
from detectron2.structures import BitMasks, Boxes, ImageList, Instances, RotatedBoxes
from detectron2.utils.env import TORCH_VERSION
from detectron2.utils.events import EventStorage
logger = logging.getLogger(__name__)
"""
Make sure the losses of ROIHeads/RPN do not change, to avoid
breaking the forward logic by mistake.
This relies on assumption that pytorch's RNG is stable.
"""
class ROIHeadsTest(unittest.TestCase):
def test_roi_heads(self):
torch.manual_seed(121)
cfg = get_cfg()
cfg.MODEL.ROI_BOX_HEAD.NAME = "FastRCNNConvFCHead"
cfg.MODEL.ROI_BOX_HEAD.NUM_FC = 2
cfg.MODEL.ROI_BOX_HEAD.POOLER_TYPE = "ROIAlignV2"
cfg.MODEL.ROI_BOX_HEAD.BBOX_REG_WEIGHTS = (10, 10, 5, 5)
cfg.MODEL.MASK_ON = True
num_images = 2
images_tensor = torch.rand(num_images, 20, 30)
image_sizes = [(10, 10), (20, 30)]
images = ImageList(images_tensor, image_sizes)
num_channels = 1024
features = {"res4": torch.rand(num_images, num_channels, 1, 2)}
feature_shape = {"res4": ShapeSpec(channels=num_channels, stride=16)}
image_shape = (15, 15)
gt_boxes0 = torch.tensor([[1, 1, 3, 3], [2, 2, 6, 6]], dtype=torch.float32)
gt_instance0 = Instances(image_shape)
gt_instance0.gt_boxes = Boxes(gt_boxes0)
gt_instance0.gt_classes = torch.tensor([2, 1])
gt_instance0.gt_masks = BitMasks(torch.rand((2,) + image_shape) > 0.5)
gt_boxes1 = torch.tensor([[1, 5, 2, 8], [7, 3, 10, 5]], dtype=torch.float32)
gt_instance1 = Instances(image_shape)
gt_instance1.gt_boxes = Boxes(gt_boxes1)
gt_instance1.gt_classes = torch.tensor([1, 2])
gt_instance1.gt_masks = BitMasks(torch.rand((2,) + image_shape) > 0.5)
gt_instances = [gt_instance0, gt_instance1]
proposal_generator = build_proposal_generator(cfg, feature_shape)
roi_heads = StandardROIHeads(cfg, feature_shape)
with EventStorage(): # capture events in a new storage to discard them
proposals, proposal_losses = proposal_generator(images, features, gt_instances)
_, detector_losses = roi_heads(images, features, proposals, gt_instances)
detector_losses.update(proposal_losses)
expected_losses = {
"loss_cls": 4.5253729820251465,
"loss_box_reg": 0.009785720147192478,
"loss_mask": 0.693184494972229,
"loss_rpn_cls": 0.08186662942171097,
"loss_rpn_loc": 0.1104838103055954,
}
succ = all(
torch.allclose(detector_losses[name], torch.tensor(expected_losses.get(name, 0.0)))
for name in detector_losses.keys()
)
self.assertTrue(
succ,
"Losses has changed! New losses: {}".format(
{k: v.item() for k, v in detector_losses.items()}
),
)
def test_rroi_heads(self):
torch.manual_seed(121)
cfg = get_cfg()
cfg.MODEL.PROPOSAL_GENERATOR.NAME = "RRPN"
cfg.MODEL.ANCHOR_GENERATOR.NAME = "RotatedAnchorGenerator"
cfg.MODEL.ROI_HEADS.NAME = "RROIHeads"
cfg.MODEL.ROI_BOX_HEAD.NAME = "FastRCNNConvFCHead"
cfg.MODEL.ROI_BOX_HEAD.NUM_FC = 2
cfg.MODEL.RPN.BBOX_REG_WEIGHTS = (1, 1, 1, 1, 1)
cfg.MODEL.RPN.HEAD_NAME = "StandardRPNHead"
cfg.MODEL.ROI_BOX_HEAD.POOLER_TYPE = "ROIAlignRotated"
cfg.MODEL.ROI_BOX_HEAD.BBOX_REG_WEIGHTS = (10, 10, 5, 5, 1)
num_images = 2
images_tensor = torch.rand(num_images, 20, 30)
image_sizes = [(10, 10), (20, 30)]
images = ImageList(images_tensor, image_sizes)
num_channels = 1024
features = {"res4": torch.rand(num_images, num_channels, 1, 2)}
feature_shape = {"res4": ShapeSpec(channels=num_channels, stride=16)}
image_shape = (15, 15)
gt_boxes0 = torch.tensor([[2, 2, 2, 2, 30], [4, 4, 4, 4, 0]], dtype=torch.float32)
gt_instance0 = Instances(image_shape)
gt_instance0.gt_boxes = RotatedBoxes(gt_boxes0)
gt_instance0.gt_classes = torch.tensor([2, 1])
gt_boxes1 = torch.tensor([[1.5, 5.5, 1, 3, 0], [8.5, 4, 3, 2, -50]], dtype=torch.float32)
gt_instance1 = Instances(image_shape)
gt_instance1.gt_boxes = RotatedBoxes(gt_boxes1)
gt_instance1.gt_classes = torch.tensor([1, 2])
gt_instances = [gt_instance0, gt_instance1]
proposal_generator = build_proposal_generator(cfg, feature_shape)
roi_heads = build_roi_heads(cfg, feature_shape)
with EventStorage(): # capture events in a new storage to discard them
proposals, proposal_losses = proposal_generator(images, features, gt_instances)
_, detector_losses = roi_heads(images, features, proposals, gt_instances)
detector_losses.update(proposal_losses)
expected_losses = {
"loss_cls": 4.365657806396484,
"loss_box_reg": 0.0015851043863222003,
"loss_rpn_cls": 0.2427729219198227,
"loss_rpn_loc": 0.3646621108055115,
}
succ = all(
torch.allclose(detector_losses[name], torch.tensor(expected_losses.get(name, 0.0)))
for name in detector_losses.keys()
)
self.assertTrue(
succ,
"Losses has changed! New losses: {}".format(
{k: v.item() for k, v in detector_losses.items()}
),
)
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_box_head_scriptability(self):
input_shape = ShapeSpec(channels=1024, height=14, width=14)
box_features = torch.randn(4, 1024, 14, 14)
box_head = FastRCNNConvFCHead(
input_shape, conv_dims=[512, 512], fc_dims=[1024, 1024]
).eval()
script_box_head = torch.jit.script(box_head)
origin_output = box_head(box_features)
script_output = script_box_head(box_features)
self.assertTrue(torch.equal(origin_output, script_output))
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_mask_head_scriptability(self):
input_shape = ShapeSpec(channels=1024)
mask_features = torch.randn(4, 1024, 14, 14)
image_shapes = [(10, 10), (15, 15)]
pred_instance0 = Instances(image_shapes[0])
pred_classes0 = torch.tensor([1, 2, 3], dtype=torch.int64)
pred_instance0.pred_classes = pred_classes0
pred_instance1 = Instances(image_shapes[1])
pred_classes1 = torch.tensor([4], dtype=torch.int64)
pred_instance1.pred_classes = pred_classes1
mask_head = MaskRCNNConvUpsampleHead(
input_shape, num_classes=80, conv_dims=[256, 256]
).eval()
# pred_instance will be in-place changed during the inference
# process of `MaskRCNNConvUpsampleHead`
origin_outputs = mask_head(mask_features, deepcopy([pred_instance0, pred_instance1]))
fields = {"pred_masks": "Tensor", "pred_classes": "Tensor"}
with patch_instances(fields) as NewInstances:
sciript_mask_head = torch.jit.script(mask_head)
pred_instance0 = NewInstances.from_instances(pred_instance0)
pred_instance1 = NewInstances.from_instances(pred_instance1)
script_outputs = sciript_mask_head(mask_features, [pred_instance0, pred_instance1])
for origin_ins, script_ins in zip(origin_outputs, script_outputs):
self.assertEqual(origin_ins.image_size, script_ins.image_size)
self.assertTrue(torch.equal(origin_ins.pred_classes, script_ins.pred_classes))
self.assertTrue(torch.equal(origin_ins.pred_masks, script_ins.pred_masks))
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_keypoint_head_scriptability(self):
input_shape = ShapeSpec(channels=1024, height=14, width=14)
keypoint_features = torch.randn(4, 1024, 14, 14)
image_shapes = [(10, 10), (15, 15)]
pred_boxes0 = torch.tensor([[1, 1, 3, 3], [2, 2, 6, 6], [1, 5, 2, 8]], dtype=torch.float32)
pred_instance0 = Instances(image_shapes[0])
pred_instance0.pred_boxes = Boxes(pred_boxes0)
pred_boxes1 = torch.tensor([[7, 3, 10, 5]], dtype=torch.float32)
pred_instance1 = Instances(image_shapes[1])
pred_instance1.pred_boxes = Boxes(pred_boxes1)
keypoint_head = KRCNNConvDeconvUpsampleHead(
input_shape, num_keypoints=17, conv_dims=[512, 512]
).eval()
origin_outputs = keypoint_head(
keypoint_features, deepcopy([pred_instance0, pred_instance1])
)
fields = {
"pred_boxes": "Boxes",
"pred_keypoints": "Tensor",
"pred_keypoint_heatmaps": "Tensor",
}
with patch_instances(fields) as NewInstances:
sciript_keypoint_head = torch.jit.script(keypoint_head)
pred_instance0 = NewInstances.from_instances(pred_instance0)
pred_instance1 = NewInstances.from_instances(pred_instance1)
script_outputs = sciript_keypoint_head(
keypoint_features, [pred_instance0, pred_instance1]
)
for origin_ins, script_ins in zip(origin_outputs, script_outputs):
self.assertEqual(origin_ins.image_size, script_ins.image_size)
self.assertTrue(torch.equal(origin_ins.pred_keypoints, script_ins.pred_keypoints))
self.assertTrue(
torch.equal(origin_ins.pred_keypoint_heatmaps, script_ins.pred_keypoint_heatmaps)
)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,139 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
import torch
from detectron2.modeling.poolers import ROIPooler
from detectron2.structures import Boxes, RotatedBoxes
from detectron2.utils.env import TORCH_VERSION
logger = logging.getLogger(__name__)
class TestROIPooler(unittest.TestCase):
def _rand_boxes(self, num_boxes, x_max, y_max):
coords = torch.rand(num_boxes, 4)
coords[:, 0] *= x_max
coords[:, 1] *= y_max
coords[:, 2] *= x_max
coords[:, 3] *= y_max
boxes = torch.zeros(num_boxes, 4)
boxes[:, 0] = torch.min(coords[:, 0], coords[:, 2])
boxes[:, 1] = torch.min(coords[:, 1], coords[:, 3])
boxes[:, 2] = torch.max(coords[:, 0], coords[:, 2])
boxes[:, 3] = torch.max(coords[:, 1], coords[:, 3])
return boxes
def _test_roialignv2_roialignrotated_match(self, device):
pooler_resolution = 14
canonical_level = 4
canonical_scale_factor = 2 ** canonical_level
pooler_scales = (1.0 / canonical_scale_factor,)
sampling_ratio = 0
N, C, H, W = 2, 4, 10, 8
N_rois = 10
std = 11
mean = 0
feature = (torch.rand(N, C, H, W) - 0.5) * 2 * std + mean
features = [feature.to(device)]
rois = []
rois_rotated = []
for _ in range(N):
boxes = self._rand_boxes(
num_boxes=N_rois, x_max=W * canonical_scale_factor, y_max=H * canonical_scale_factor
)
rotated_boxes = torch.zeros(N_rois, 5)
rotated_boxes[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2.0
rotated_boxes[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2.0
rotated_boxes[:, 2] = boxes[:, 2] - boxes[:, 0]
rotated_boxes[:, 3] = boxes[:, 3] - boxes[:, 1]
rois.append(Boxes(boxes).to(device))
rois_rotated.append(RotatedBoxes(rotated_boxes).to(device))
roialignv2_pooler = ROIPooler(
output_size=pooler_resolution,
scales=pooler_scales,
sampling_ratio=sampling_ratio,
pooler_type="ROIAlignV2",
)
roialignv2_out = roialignv2_pooler(features, rois)
roialignrotated_pooler = ROIPooler(
output_size=pooler_resolution,
scales=pooler_scales,
sampling_ratio=sampling_ratio,
pooler_type="ROIAlignRotated",
)
roialignrotated_out = roialignrotated_pooler(features, rois_rotated)
self.assertTrue(torch.allclose(roialignv2_out, roialignrotated_out, atol=1e-4))
def test_roialignv2_roialignrotated_match_cpu(self):
self._test_roialignv2_roialignrotated_match(device="cpu")
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_roialignv2_roialignrotated_match_cuda(self):
self._test_roialignv2_roialignrotated_match(device="cuda")
def _test_scriptability(self, device):
pooler_resolution = 14
canonical_level = 4
canonical_scale_factor = 2 ** canonical_level
pooler_scales = (1.0 / canonical_scale_factor,)
sampling_ratio = 0
N, C, H, W = 2, 4, 10, 8
N_rois = 10
std = 11
mean = 0
feature = (torch.rand(N, C, H, W) - 0.5) * 2 * std + mean
features = [feature.to(device)]
rois = []
for _ in range(N):
boxes = self._rand_boxes(
num_boxes=N_rois, x_max=W * canonical_scale_factor, y_max=H * canonical_scale_factor
)
rois.append(Boxes(boxes).to(device))
roialignv2_pooler = ROIPooler(
output_size=pooler_resolution,
scales=pooler_scales,
sampling_ratio=sampling_ratio,
pooler_type="ROIAlignV2",
)
roialignv2_out = roialignv2_pooler(features, rois)
scripted_roialignv2_out = torch.jit.script(roialignv2_pooler)(features, rois)
self.assertTrue(torch.equal(roialignv2_out, scripted_roialignv2_out))
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_scriptability_cpu(self):
self._test_scriptability(device="cpu")
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_scriptability_gpu(self):
self._test_scriptability(device="cuda")
def test_no_images(self):
N, C, H, W = 0, 32, 32, 32
feature = torch.rand(N, C, H, W) - 0.5
features = [feature]
pooler = ROIPooler(
output_size=14, scales=(1.0,), sampling_ratio=0.0, pooler_type="ROIAlignV2"
)
output = pooler.forward(features, [])
self.assertEqual(output.shape, (0, C, 14, 14))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,256 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
import torch
from detectron2.config import get_cfg
from detectron2.export.torchscript import export_torchscript_with_instances
from detectron2.layers import ShapeSpec
from detectron2.modeling.backbone import build_backbone
from detectron2.modeling.proposal_generator import RPN, build_proposal_generator
from detectron2.modeling.proposal_generator.proposal_utils import find_top_rpn_proposals
from detectron2.structures import Boxes, ImageList, Instances, RotatedBoxes
from detectron2.utils.env import TORCH_VERSION
from detectron2.utils.events import EventStorage
logger = logging.getLogger(__name__)
class RPNTest(unittest.TestCase):
def test_rpn(self):
torch.manual_seed(121)
cfg = get_cfg()
backbone = build_backbone(cfg)
proposal_generator = RPN(cfg, backbone.output_shape())
num_images = 2
images_tensor = torch.rand(num_images, 20, 30)
image_sizes = [(10, 10), (20, 30)]
images = ImageList(images_tensor, image_sizes)
image_shape = (15, 15)
num_channels = 1024
features = {"res4": torch.rand(num_images, num_channels, 1, 2)}
gt_boxes = torch.tensor([[1, 1, 3, 3], [2, 2, 6, 6]], dtype=torch.float32)
gt_instances = Instances(image_shape)
gt_instances.gt_boxes = Boxes(gt_boxes)
with EventStorage(): # capture events in a new storage to discard them
proposals, proposal_losses = proposal_generator(
images, features, [gt_instances[0], gt_instances[1]]
)
expected_losses = {
"loss_rpn_cls": torch.tensor(0.0804563984),
"loss_rpn_loc": torch.tensor(0.0990132466),
}
for name in expected_losses.keys():
err_msg = "proposal_losses[{}] = {}, expected losses = {}".format(
name, proposal_losses[name], expected_losses[name]
)
self.assertTrue(torch.allclose(proposal_losses[name], expected_losses[name]), err_msg)
expected_proposal_boxes = [
Boxes(torch.tensor([[0, 0, 10, 10], [7.3365392685, 0, 10, 10]])),
Boxes(
torch.tensor(
[
[0, 0, 30, 20],
[0, 0, 16.7862777710, 13.1362524033],
[0, 0, 30, 13.3173446655],
[0, 0, 10.8602609634, 20],
[7.7165775299, 0, 27.3875980377, 20],
]
)
),
]
expected_objectness_logits = [
torch.tensor([0.1225359365, -0.0133192837]),
torch.tensor([0.1415634006, 0.0989848152, 0.0565387346, -0.0072308783, -0.0428492837]),
]
for proposal, expected_proposal_box, im_size, expected_objectness_logit in zip(
proposals, expected_proposal_boxes, image_sizes, expected_objectness_logits
):
self.assertEqual(len(proposal), len(expected_proposal_box))
self.assertEqual(proposal.image_size, im_size)
self.assertTrue(
torch.allclose(proposal.proposal_boxes.tensor, expected_proposal_box.tensor)
)
self.assertTrue(torch.allclose(proposal.objectness_logits, expected_objectness_logit))
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_rpn_scriptability(self):
cfg = get_cfg()
proposal_generator = RPN(cfg, {"res4": ShapeSpec(channels=1024, stride=16)}).eval()
num_images = 2
images_tensor = torch.rand(num_images, 30, 40)
image_sizes = [(32, 32), (30, 40)]
images = ImageList(images_tensor, image_sizes)
features = {"res4": torch.rand(num_images, 1024, 1, 2)}
fields = {"proposal_boxes": "Boxes", "objectness_logits": "Tensor"}
proposal_generator_ts = export_torchscript_with_instances(proposal_generator, fields)
proposals, _ = proposal_generator(images, features)
proposals_ts, _ = proposal_generator_ts(images, features)
for proposal, proposal_ts in zip(proposals, proposals_ts):
self.assertEqual(proposal.image_size, proposal_ts.image_size)
self.assertTrue(
torch.equal(proposal.proposal_boxes.tensor, proposal_ts.proposal_boxes.tensor)
)
self.assertTrue(torch.equal(proposal.objectness_logits, proposal_ts.objectness_logits))
def test_rrpn(self):
torch.manual_seed(121)
cfg = get_cfg()
cfg.MODEL.PROPOSAL_GENERATOR.NAME = "RRPN"
cfg.MODEL.ANCHOR_GENERATOR.NAME = "RotatedAnchorGenerator"
cfg.MODEL.ANCHOR_GENERATOR.SIZES = [[32, 64]]
cfg.MODEL.ANCHOR_GENERATOR.ASPECT_RATIOS = [[0.25, 1]]
cfg.MODEL.ANCHOR_GENERATOR.ANGLES = [[0, 60]]
cfg.MODEL.RPN.BBOX_REG_WEIGHTS = (1, 1, 1, 1, 1)
cfg.MODEL.RPN.HEAD_NAME = "StandardRPNHead"
backbone = build_backbone(cfg)
proposal_generator = build_proposal_generator(cfg, backbone.output_shape())
num_images = 2
images_tensor = torch.rand(num_images, 20, 30)
image_sizes = [(10, 10), (20, 30)]
images = ImageList(images_tensor, image_sizes)
image_shape = (15, 15)
num_channels = 1024
features = {"res4": torch.rand(num_images, num_channels, 1, 2)}
gt_boxes = torch.tensor([[2, 2, 2, 2, 0], [4, 4, 4, 4, 0]], dtype=torch.float32)
gt_instances = Instances(image_shape)
gt_instances.gt_boxes = RotatedBoxes(gt_boxes)
with EventStorage(): # capture events in a new storage to discard them
proposals, proposal_losses = proposal_generator(
images, features, [gt_instances[0], gt_instances[1]]
)
expected_losses = {
"loss_rpn_cls": torch.tensor(0.043263837695121765),
"loss_rpn_loc": torch.tensor(0.14432406425476074),
}
for name in expected_losses.keys():
err_msg = "proposal_losses[{}] = {}, expected losses = {}".format(
name, proposal_losses[name], expected_losses[name]
)
self.assertTrue(torch.allclose(proposal_losses[name], expected_losses[name]), err_msg)
expected_proposal_boxes = [
RotatedBoxes(
torch.tensor(
[
[0.60189795, 1.24095452, 61.98131943, 18.03621292, -4.07244873],
[15.64940453, 1.69624567, 59.59749603, 16.34339333, 2.62692475],
[-3.02982378, -2.69752932, 67.90952301, 59.62455750, 59.97010040],
[16.71863365, 1.98309708, 35.61507797, 32.81484985, 62.92267227],
[0.49432933, -7.92979717, 67.77606201, 62.93098450, -1.85656738],
[8.00880814, 1.36017394, 121.81007385, 32.74150467, 50.44297409],
[16.44299889, -4.82221127, 63.39775848, 61.22503662, 54.12270737],
[5.00000000, 5.00000000, 10.00000000, 10.00000000, -0.76943970],
[17.64130402, -0.98095351, 61.40377808, 16.28918839, 55.53118134],
[0.13016054, 4.60568953, 35.80157471, 32.30180359, 62.52872086],
[-4.26460743, 0.39604485, 124.30079651, 31.84611320, -1.58203125],
[7.52815342, -0.91636634, 62.39784622, 15.45565224, 60.79549789],
]
)
),
RotatedBoxes(
torch.tensor(
[
[0.07734215, 0.81635046, 65.33510590, 17.34688377, -1.51821899],
[-3.41833067, -3.11320257, 64.17595673, 60.55617905, 58.27033234],
[20.67383385, -6.16561556, 63.60531998, 62.52315903, 54.85546494],
[15.00000000, 10.00000000, 30.00000000, 20.00000000, -0.18218994],
[9.22646523, -6.84775209, 62.09895706, 65.46472931, -2.74307251],
[15.00000000, 4.93451595, 30.00000000, 9.86903191, -0.60272217],
[8.88342094, 2.65560246, 120.95362854, 32.45022202, 55.75970078],
[16.39088631, 2.33887148, 34.78761292, 35.61492920, 60.81977463],
[9.78298569, 10.00000000, 19.56597137, 20.00000000, -0.86660767],
[1.28576660, 5.49873352, 34.93610382, 33.22600174, 60.51599884],
[17.58912468, -1.63270092, 62.96052551, 16.45713997, 52.91245270],
[5.64749718, -1.90428460, 62.37649155, 16.19474792, 61.09543991],
[0.82255805, 2.34931135, 118.83985901, 32.83671188, 56.50753784],
[-5.33874989, 1.64404404, 125.28501892, 33.35424042, -2.80731201],
]
)
),
]
expected_objectness_logits = [
torch.tensor(
[
0.10111768,
0.09112845,
0.08466332,
0.07589971,
0.06650183,
0.06350251,
0.04299347,
0.01864817,
0.00986163,
0.00078543,
-0.04573630,
-0.04799230,
]
),
torch.tensor(
[
0.11373727,
0.09377633,
0.05281663,
0.05143715,
0.04040275,
0.03250912,
0.01307789,
0.01177734,
0.00038105,
-0.00540255,
-0.01194804,
-0.01461012,
-0.03061717,
-0.03599222,
]
),
]
torch.set_printoptions(precision=8, sci_mode=False)
for proposal, expected_proposal_box, im_size, expected_objectness_logit in zip(
proposals, expected_proposal_boxes, image_sizes, expected_objectness_logits
):
self.assertEqual(len(proposal), len(expected_proposal_box))
self.assertEqual(proposal.image_size, im_size)
# It seems that there's some randomness in the result across different machines:
# This test can be run on a local machine for 100 times with exactly the same result,
# However, a different machine might produce slightly different results,
# thus the atol here.
err_msg = "computed proposal boxes = {}, expected {}".format(
proposal.proposal_boxes.tensor, expected_proposal_box.tensor
)
self.assertTrue(
torch.allclose(
proposal.proposal_boxes.tensor, expected_proposal_box.tensor, atol=1e-5
),
err_msg,
)
err_msg = "computed objectness logits = {}, expected {}".format(
proposal.objectness_logits, expected_objectness_logit
)
self.assertTrue(
torch.allclose(proposal.objectness_logits, expected_objectness_logit, atol=1e-5),
err_msg,
)
def test_rpn_proposals_inf(self):
N, Hi, Wi, A = 3, 3, 3, 3
proposals = [torch.rand(N, Hi * Wi * A, 4)]
pred_logits = [torch.rand(N, Hi * Wi * A)]
pred_logits[0][1][3:5].fill_(float("inf"))
find_top_rpn_proposals(proposals, pred_logits, [(10, 10)], 0.5, 1000, 1000, 0, False)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,203 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import json
import math
import numpy as np
import unittest
import torch
from detectron2.structures import Boxes, BoxMode, pairwise_ioa, pairwise_iou
from detectron2.utils.env import TORCH_VERSION
class TestBoxMode(unittest.TestCase):
def _convert_xy_to_wh(self, x):
return BoxMode.convert(x, BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
def _convert_xywha_to_xyxy(self, x):
return BoxMode.convert(x, BoxMode.XYWHA_ABS, BoxMode.XYXY_ABS)
def _convert_xywh_to_xywha(self, x):
return BoxMode.convert(x, BoxMode.XYWH_ABS, BoxMode.XYWHA_ABS)
def test_box_convert_list(self):
for tp in [list, tuple]:
box = tp([5.0, 5.0, 10.0, 10.0])
output = self._convert_xy_to_wh(box)
self.assertIsInstance(output, tp)
self.assertIsInstance(output[0], float)
self.assertEqual(output, tp([5.0, 5.0, 5.0, 5.0]))
with self.assertRaises(Exception):
self._convert_xy_to_wh([box])
def test_box_convert_array(self):
box = np.asarray([[5, 5, 10, 10], [1, 1, 2, 3]])
output = self._convert_xy_to_wh(box)
self.assertEqual(output.dtype, box.dtype)
self.assertEqual(output.shape, box.shape)
self.assertTrue((output[0] == [5, 5, 5, 5]).all())
self.assertTrue((output[1] == [1, 1, 1, 2]).all())
def test_box_convert_cpu_tensor(self):
box = torch.tensor([[5, 5, 10, 10], [1, 1, 2, 3]])
output = self._convert_xy_to_wh(box)
self.assertEqual(output.dtype, box.dtype)
self.assertEqual(output.shape, box.shape)
output = output.numpy()
self.assertTrue((output[0] == [5, 5, 5, 5]).all())
self.assertTrue((output[1] == [1, 1, 1, 2]).all())
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_box_convert_cuda_tensor(self):
box = torch.tensor([[5, 5, 10, 10], [1, 1, 2, 3]]).cuda()
output = self._convert_xy_to_wh(box)
self.assertEqual(output.dtype, box.dtype)
self.assertEqual(output.shape, box.shape)
self.assertEqual(output.device, box.device)
output = output.cpu().numpy()
self.assertTrue((output[0] == [5, 5, 5, 5]).all())
self.assertTrue((output[1] == [1, 1, 1, 2]).all())
def test_box_convert_xywha_to_xyxy_list(self):
for tp in [list, tuple]:
box = tp([50, 50, 30, 20, 0])
output = self._convert_xywha_to_xyxy(box)
self.assertIsInstance(output, tp)
self.assertEqual(output, tp([35, 40, 65, 60]))
with self.assertRaises(Exception):
self._convert_xywha_to_xyxy([box])
def test_box_convert_xywha_to_xyxy_array(self):
for dtype in [np.float64, np.float32]:
box = np.asarray(
[
[50, 50, 30, 20, 0],
[50, 50, 30, 20, 90],
[1, 1, math.sqrt(2), math.sqrt(2), -45],
],
dtype=dtype,
)
output = self._convert_xywha_to_xyxy(box)
self.assertEqual(output.dtype, box.dtype)
expected = np.asarray([[35, 40, 65, 60], [40, 35, 60, 65], [0, 0, 2, 2]], dtype=dtype)
self.assertTrue(np.allclose(output, expected, atol=1e-6), "output={}".format(output))
def test_box_convert_xywha_to_xyxy_tensor(self):
for dtype in [torch.float32, torch.float64]:
box = torch.tensor(
[
[50, 50, 30, 20, 0],
[50, 50, 30, 20, 90],
[1, 1, math.sqrt(2), math.sqrt(2), -45],
],
dtype=dtype,
)
output = self._convert_xywha_to_xyxy(box)
self.assertEqual(output.dtype, box.dtype)
expected = torch.tensor([[35, 40, 65, 60], [40, 35, 60, 65], [0, 0, 2, 2]], dtype=dtype)
self.assertTrue(torch.allclose(output, expected, atol=1e-6), "output={}".format(output))
def test_box_convert_xywh_to_xywha_list(self):
for tp in [list, tuple]:
box = tp([50, 50, 30, 20])
output = self._convert_xywh_to_xywha(box)
self.assertIsInstance(output, tp)
self.assertEqual(output, tp([65, 60, 30, 20, 0]))
with self.assertRaises(Exception):
self._convert_xywh_to_xywha([box])
def test_box_convert_xywh_to_xywha_array(self):
for dtype in [np.float64, np.float32]:
box = np.asarray([[30, 40, 70, 60], [30, 40, 60, 70], [-1, -1, 2, 2]], dtype=dtype)
output = self._convert_xywh_to_xywha(box)
self.assertEqual(output.dtype, box.dtype)
expected = np.asarray(
[[65, 70, 70, 60, 0], [60, 75, 60, 70, 0], [0, 0, 2, 2, 0]], dtype=dtype
)
self.assertTrue(np.allclose(output, expected, atol=1e-6), "output={}".format(output))
def test_box_convert_xywh_to_xywha_tensor(self):
for dtype in [torch.float32, torch.float64]:
box = torch.tensor([[30, 40, 70, 60], [30, 40, 60, 70], [-1, -1, 2, 2]], dtype=dtype)
output = self._convert_xywh_to_xywha(box)
self.assertEqual(output.dtype, box.dtype)
expected = torch.tensor(
[[65, 70, 70, 60, 0], [60, 75, 60, 70, 0], [0, 0, 2, 2, 0]], dtype=dtype
)
self.assertTrue(torch.allclose(output, expected, atol=1e-6), "output={}".format(output))
def test_json_serializable(self):
payload = {"box_mode": BoxMode.XYWH_REL}
try:
json.dumps(payload)
except Exception:
self.fail("JSON serialization failed")
def test_json_deserializable(self):
payload = '{"box_mode": 2}'
obj = json.loads(payload)
try:
obj["box_mode"] = BoxMode(obj["box_mode"])
except Exception:
self.fail("JSON deserialization failed")
class TestBoxIOU(unittest.TestCase):
def create_boxes(self):
boxes1 = torch.tensor([[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]])
boxes2 = torch.tensor(
[
[0.0, 0.0, 1.0, 1.0],
[0.0, 0.0, 0.5, 1.0],
[0.0, 0.0, 1.0, 0.5],
[0.0, 0.0, 0.5, 0.5],
[0.5, 0.5, 1.0, 1.0],
[0.5, 0.5, 1.5, 1.5],
]
)
return boxes1, boxes2
def test_pairwise_iou(self):
boxes1, boxes2 = self.create_boxes()
expected_ious = torch.tensor(
[
[1.0, 0.5, 0.5, 0.25, 0.25, 0.25 / (2 - 0.25)],
[1.0, 0.5, 0.5, 0.25, 0.25, 0.25 / (2 - 0.25)],
]
)
ious = pairwise_iou(Boxes(boxes1), Boxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_pairwise_ioa(self):
boxes1, boxes2 = self.create_boxes()
expected_ioas = torch.tensor(
[[1.0, 1.0, 1.0, 1.0, 1.0, 0.25], [1.0, 1.0, 1.0, 1.0, 1.0, 0.25]]
)
ioas = pairwise_ioa(Boxes(boxes1), Boxes(boxes2))
self.assertTrue(torch.allclose(ioas, expected_ioas))
class TestBoxes(unittest.TestCase):
def test_empty_cat(self):
x = Boxes.cat([])
self.assertTrue(x.tensor.shape, (0, 4))
# require https://github.com/pytorch/pytorch/pull/39336
@unittest.skipIf(TORCH_VERSION < (1, 6), "Insufficient pytorch version")
def test_scriptability(self):
def func(x):
boxes = Boxes(x)
return boxes.area()
f = torch.jit.script(func)
f(torch.rand((3, 4)))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,59 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import unittest
from typing import List, Sequence, Tuple
import torch
from detectron2.structures import ImageList
from detectron2.utils.env import TORCH_VERSION
class TestImageList(unittest.TestCase):
def test_imagelist_padding_shape(self):
class TensorToImageList(torch.nn.Module):
def forward(self, tensors: Sequence[torch.Tensor]):
return ImageList.from_tensors(tensors, 4).tensor
func = torch.jit.trace(
TensorToImageList(), ([torch.ones((3, 10, 10), dtype=torch.float32)],)
)
ret = func([torch.ones((3, 15, 20), dtype=torch.float32)])
self.assertEqual(list(ret.shape), [1, 3, 16, 20], str(ret.shape))
func = torch.jit.trace(
TensorToImageList(),
(
[
torch.ones((3, 16, 10), dtype=torch.float32),
torch.ones((3, 13, 11), dtype=torch.float32),
],
),
)
ret = func(
[
torch.ones((3, 25, 20), dtype=torch.float32),
torch.ones((3, 10, 10), dtype=torch.float32),
]
)
# does not support calling with different #images
self.assertEqual(list(ret.shape), [2, 3, 28, 20], str(ret.shape))
@unittest.skipIf(TORCH_VERSION < (1, 6), "Insufficient pytorch version")
def test_imagelist_scriptability(self):
image_nums = 2
image_tensor = torch.randn((image_nums, 10, 20), dtype=torch.float32)
image_shape = [(10, 20)] * image_nums
def f(image_tensor, image_shape: List[Tuple[int, int]]):
return ImageList(image_tensor, image_shape)
ret = f(image_tensor, image_shape)
ret_script = torch.jit.script(f)(image_tensor, image_shape)
self.assertEqual(len(ret), len(ret_script))
for i in range(image_nums):
self.assertTrue(torch.equal(ret[i], ret_script[i]))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,120 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import unittest
import torch
from detectron2.export.torchscript import patch_instances
from detectron2.structures import Boxes, Instances
from detectron2.utils.env import TORCH_VERSION
class TestInstances(unittest.TestCase):
def test_int_indexing(self):
attr1 = torch.tensor([[0.0, 0.0, 1.0], [0.0, 0.0, 0.5], [0.0, 0.0, 1.0], [0.0, 0.5, 0.5]])
attr2 = torch.tensor([0.1, 0.2, 0.3, 0.4])
instances = Instances((100, 100))
instances.attr1 = attr1
instances.attr2 = attr2
for i in range(-len(instances), len(instances)):
inst = instances[i]
self.assertEqual((inst.attr1 == attr1[i]).all(), True)
self.assertEqual((inst.attr2 == attr2[i]).all(), True)
self.assertRaises(IndexError, lambda: instances[len(instances)])
self.assertRaises(IndexError, lambda: instances[-len(instances) - 1])
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_script_new_fields(self):
class f(torch.nn.Module):
def forward(self, x: Instances):
proposal_boxes = x.proposal_boxes # noqa F841
objectness_logits = x.objectness_logits # noqa F841
return x
class g(torch.nn.Module):
def forward(self, x: Instances):
mask = x.mask # noqa F841
return x
class g2(torch.nn.Module):
def forward(self, x: Instances):
proposal_boxes = x.proposal_boxes # noqa F841
return x
fields = {"proposal_boxes": "Boxes", "objectness_logits": "Tensor"}
with patch_instances(fields):
torch.jit.script(f())
# can't script anymore after exiting the context
with self.assertRaises(Exception):
torch.jit.script(g2())
new_fields = {"mask": "Tensor"}
with patch_instances(new_fields):
torch.jit.script(g())
with self.assertRaises(Exception):
torch.jit.script(g2())
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_script_access_fields(self):
class f(torch.nn.Module):
def forward(self, x: Instances):
proposal_boxes = x.proposal_boxes
objectness_logits = x.objectness_logits
return proposal_boxes.tensor + objectness_logits
fields = {"proposal_boxes": "Boxes", "objectness_logits": "Tensor"}
with patch_instances(fields):
torch.jit.script(f())
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_script_len(self):
class f(torch.nn.Module):
def forward(self, x: Instances):
return len(x)
class g(torch.nn.Module):
def forward(self, x: Instances):
return len(x)
image_shape = (15, 15)
fields = {"proposal_boxes": "Boxes"}
with patch_instances(fields) as new_instance:
script_module = torch.jit.script(f())
x = new_instance(image_shape)
with self.assertRaises(Exception):
script_module(x)
box_tensors = torch.tensor([[5, 5, 10, 10], [1, 1, 2, 3]])
x.proposal_boxes = Boxes(box_tensors)
length = script_module(x)
self.assertEqual(length, 2)
fields = {"objectness_logits": "Tensor"}
with patch_instances(fields) as new_instance:
script_module = torch.jit.script(g())
x = new_instance(image_shape)
objectness_logits = torch.tensor([1.0]).reshape(1, 1)
x.objectness_logits = objectness_logits
length = script_module(x)
self.assertEqual(length, 1)
@unittest.skipIf(TORCH_VERSION < (1, 7), "Insufficient pytorch version")
def test_script_has(self):
class f(torch.nn.Module):
def forward(self, x: Instances):
return x.has("proposal_boxes")
image_shape = (15, 15)
fields = {"proposal_boxes": "Boxes"}
with patch_instances(fields) as new_instance:
script_module = torch.jit.script(f())
x = new_instance(image_shape)
self.assertFalse(script_module(x))
box_tensors = torch.tensor([[5, 5, 10, 10], [1, 1, 2, 3]])
x.proposal_boxes = Boxes(box_tensors)
self.assertTrue(script_module(x))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,42 @@
import unittest
import torch
from detectron2.structures.masks import BitMasks, PolygonMasks, polygons_to_bitmask
class TestBitMask(unittest.TestCase):
def test_get_bounding_box(self):
masks = torch.tensor(
[
[
[False, False, False, True],
[False, False, True, True],
[False, True, True, False],
[False, True, True, False],
],
[
[False, False, False, False],
[False, False, True, False],
[False, True, True, False],
[False, True, True, False],
],
torch.zeros(4, 4),
]
)
bitmask = BitMasks(masks)
box_true = torch.tensor([[1, 0, 4, 4], [1, 1, 3, 4], [0, 0, 0, 0]], dtype=torch.float32)
box = bitmask.get_bounding_boxes()
self.assertTrue(torch.all(box.tensor == box_true).item())
for box in box_true:
poly = box[[0, 1, 2, 1, 2, 3, 0, 3]].numpy()
mask = polygons_to_bitmask([poly], 4, 4)
reconstruct_box = BitMasks(mask[None, :, :]).get_bounding_boxes()[0].tensor
self.assertTrue(torch.all(box == reconstruct_box).item())
reconstruct_box = PolygonMasks([[poly]]).get_bounding_boxes()[0].tensor
self.assertTrue(torch.all(box == reconstruct_box).item())
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,357 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
from __future__ import absolute_import, division, print_function, unicode_literals
import logging
import math
import random
import unittest
import torch
from fvcore.common.benchmark import benchmark
from detectron2.layers.rotated_boxes import pairwise_iou_rotated
from detectron2.structures.boxes import Boxes
from detectron2.structures.rotated_boxes import RotatedBoxes, pairwise_iou
logger = logging.getLogger(__name__)
class TestRotatedBoxesLayer(unittest.TestCase):
def test_iou_0_dim_cpu(self):
boxes1 = torch.rand(0, 5, dtype=torch.float32)
boxes2 = torch.rand(10, 5, dtype=torch.float32)
expected_ious = torch.zeros(0, 10, dtype=torch.float32)
ious = pairwise_iou_rotated(boxes1, boxes2)
self.assertTrue(torch.allclose(ious, expected_ious))
boxes1 = torch.rand(10, 5, dtype=torch.float32)
boxes2 = torch.rand(0, 5, dtype=torch.float32)
expected_ious = torch.zeros(10, 0, dtype=torch.float32)
ious = pairwise_iou_rotated(boxes1, boxes2)
self.assertTrue(torch.allclose(ious, expected_ious))
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_iou_0_dim_cuda(self):
boxes1 = torch.rand(0, 5, dtype=torch.float32)
boxes2 = torch.rand(10, 5, dtype=torch.float32)
expected_ious = torch.zeros(0, 10, dtype=torch.float32)
ious_cuda = pairwise_iou_rotated(boxes1.cuda(), boxes2.cuda())
self.assertTrue(torch.allclose(ious_cuda.cpu(), expected_ious))
boxes1 = torch.rand(10, 5, dtype=torch.float32)
boxes2 = torch.rand(0, 5, dtype=torch.float32)
expected_ious = torch.zeros(10, 0, dtype=torch.float32)
ious_cuda = pairwise_iou_rotated(boxes1.cuda(), boxes2.cuda())
self.assertTrue(torch.allclose(ious_cuda.cpu(), expected_ious))
def test_iou_half_overlap_cpu(self):
boxes1 = torch.tensor([[0.5, 0.5, 1.0, 1.0, 0.0]], dtype=torch.float32)
boxes2 = torch.tensor([[0.25, 0.5, 0.5, 1.0, 0.0]], dtype=torch.float32)
expected_ious = torch.tensor([[0.5]], dtype=torch.float32)
ious = pairwise_iou_rotated(boxes1, boxes2)
self.assertTrue(torch.allclose(ious, expected_ious))
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_iou_half_overlap_cuda(self):
boxes1 = torch.tensor([[0.5, 0.5, 1.0, 1.0, 0.0]], dtype=torch.float32)
boxes2 = torch.tensor([[0.25, 0.5, 0.5, 1.0, 0.0]], dtype=torch.float32)
expected_ious = torch.tensor([[0.5]], dtype=torch.float32)
ious_cuda = pairwise_iou_rotated(boxes1.cuda(), boxes2.cuda())
self.assertTrue(torch.allclose(ious_cuda.cpu(), expected_ious))
def test_iou_precision(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
boxes1 = torch.tensor([[565, 565, 10, 10.0, 0]], dtype=torch.float32, device=device)
boxes2 = torch.tensor([[565, 565, 10, 8.3, 0]], dtype=torch.float32, device=device)
iou = 8.3 / 10.0
expected_ious = torch.tensor([[iou]], dtype=torch.float32)
ious = pairwise_iou_rotated(boxes1, boxes2)
self.assertTrue(torch.allclose(ious.cpu(), expected_ious))
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_iou_too_many_boxes_cuda(self):
s1, s2 = 5, 1289035
boxes1 = torch.zeros(s1, 5)
boxes2 = torch.zeros(s2, 5)
ious_cuda = pairwise_iou_rotated(boxes1.cuda(), boxes2.cuda())
self.assertTupleEqual(tuple(ious_cuda.shape), (s1, s2))
def test_iou_extreme(self):
# Cause floating point issues in cuda kernels (#1266)
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
boxes1 = torch.tensor([[160.0, 153.0, 230.0, 23.0, -37.0]], device=device)
boxes2 = torch.tensor(
[
[
-1.117407639806935e17,
1.3858420478349148e18,
1000.0000610351562,
1000.0000610351562,
1612.0,
]
],
device=device,
)
ious = pairwise_iou_rotated(boxes1, boxes2)
self.assertTrue(ious.min() >= 0, ious)
class TestRotatedBoxesStructure(unittest.TestCase):
def test_clip_area_0_degree(self):
for _ in range(50):
num_boxes = 100
boxes_5d = torch.zeros(num_boxes, 5)
boxes_5d[:, 0] = torch.FloatTensor(num_boxes).uniform_(-100, 500)
boxes_5d[:, 1] = torch.FloatTensor(num_boxes).uniform_(-100, 500)
boxes_5d[:, 2] = torch.FloatTensor(num_boxes).uniform_(0, 500)
boxes_5d[:, 3] = torch.FloatTensor(num_boxes).uniform_(0, 500)
# Convert from (x_ctr, y_ctr, w, h, 0) to (x1, y1, x2, y2)
boxes_4d = torch.zeros(num_boxes, 4)
boxes_4d[:, 0] = boxes_5d[:, 0] - boxes_5d[:, 2] / 2.0
boxes_4d[:, 1] = boxes_5d[:, 1] - boxes_5d[:, 3] / 2.0
boxes_4d[:, 2] = boxes_5d[:, 0] + boxes_5d[:, 2] / 2.0
boxes_4d[:, 3] = boxes_5d[:, 1] + boxes_5d[:, 3] / 2.0
image_size = (500, 600)
test_boxes_4d = Boxes(boxes_4d)
test_boxes_5d = RotatedBoxes(boxes_5d)
# Before clip
areas_4d = test_boxes_4d.area()
areas_5d = test_boxes_5d.area()
self.assertTrue(torch.allclose(areas_4d, areas_5d, atol=1e-1, rtol=1e-5))
# After clip
test_boxes_4d.clip(image_size)
test_boxes_5d.clip(image_size)
areas_4d = test_boxes_4d.area()
areas_5d = test_boxes_5d.area()
self.assertTrue(torch.allclose(areas_4d, areas_5d, atol=1e-1, rtol=1e-5))
def test_clip_area_arbitrary_angle(self):
num_boxes = 100
boxes_5d = torch.zeros(num_boxes, 5)
boxes_5d[:, 0] = torch.FloatTensor(num_boxes).uniform_(-100, 500)
boxes_5d[:, 1] = torch.FloatTensor(num_boxes).uniform_(-100, 500)
boxes_5d[:, 2] = torch.FloatTensor(num_boxes).uniform_(0, 500)
boxes_5d[:, 3] = torch.FloatTensor(num_boxes).uniform_(0, 500)
boxes_5d[:, 4] = torch.FloatTensor(num_boxes).uniform_(-1800, 1800)
clip_angle_threshold = random.uniform(0, 180)
image_size = (500, 600)
test_boxes_5d = RotatedBoxes(boxes_5d)
# Before clip
areas_before = test_boxes_5d.area()
# After clip
test_boxes_5d.clip(image_size, clip_angle_threshold)
areas_diff = test_boxes_5d.area() - areas_before
# the areas should only decrease after clipping
self.assertTrue(torch.all(areas_diff <= 0))
# whenever the box is clipped (thus the area shrinks),
# the angle for the box must be within the clip_angle_threshold
# Note that the clip function will normalize the angle range
# to be within (-180, 180]
self.assertTrue(
torch.all(torch.abs(boxes_5d[:, 4][torch.where(areas_diff < 0)]) < clip_angle_threshold)
)
def test_normalize_angles(self):
# torch.manual_seed(0)
for _ in range(50):
num_boxes = 100
boxes_5d = torch.zeros(num_boxes, 5)
boxes_5d[:, 0] = torch.FloatTensor(num_boxes).uniform_(-100, 500)
boxes_5d[:, 1] = torch.FloatTensor(num_boxes).uniform_(-100, 500)
boxes_5d[:, 2] = torch.FloatTensor(num_boxes).uniform_(0, 500)
boxes_5d[:, 3] = torch.FloatTensor(num_boxes).uniform_(0, 500)
boxes_5d[:, 4] = torch.FloatTensor(num_boxes).uniform_(-1800, 1800)
rotated_boxes = RotatedBoxes(boxes_5d)
normalized_boxes = rotated_boxes.clone()
normalized_boxes.normalize_angles()
self.assertTrue(torch.all(normalized_boxes.tensor[:, 4] >= -180))
self.assertTrue(torch.all(normalized_boxes.tensor[:, 4] < 180))
# x, y, w, h should not change
self.assertTrue(torch.allclose(boxes_5d[:, :4], normalized_boxes.tensor[:, :4]))
# the cos/sin values of the angles should stay the same
self.assertTrue(
torch.allclose(
torch.cos(boxes_5d[:, 4] * math.pi / 180),
torch.cos(normalized_boxes.tensor[:, 4] * math.pi / 180),
atol=1e-5,
)
)
self.assertTrue(
torch.allclose(
torch.sin(boxes_5d[:, 4] * math.pi / 180),
torch.sin(normalized_boxes.tensor[:, 4] * math.pi / 180),
atol=1e-5,
)
)
def test_pairwise_iou_0_degree(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
boxes1 = torch.tensor(
[[0.5, 0.5, 1.0, 1.0, 0.0], [0.5, 0.5, 1.0, 1.0, 0.0]],
dtype=torch.float32,
device=device,
)
boxes2 = torch.tensor(
[
[0.5, 0.5, 1.0, 1.0, 0.0],
[0.25, 0.5, 0.5, 1.0, 0.0],
[0.5, 0.25, 1.0, 0.5, 0.0],
[0.25, 0.25, 0.5, 0.5, 0.0],
[0.75, 0.75, 0.5, 0.5, 0.0],
[1.0, 1.0, 1.0, 1.0, 0.0],
],
dtype=torch.float32,
device=device,
)
expected_ious = torch.tensor(
[
[1.0, 0.5, 0.5, 0.25, 0.25, 0.25 / (2 - 0.25)],
[1.0, 0.5, 0.5, 0.25, 0.25, 0.25 / (2 - 0.25)],
],
dtype=torch.float32,
device=device,
)
ious = pairwise_iou(RotatedBoxes(boxes1), RotatedBoxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_pairwise_iou_45_degrees(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
boxes1 = torch.tensor(
[
[1, 1, math.sqrt(2), math.sqrt(2), 45],
[1, 1, 2 * math.sqrt(2), 2 * math.sqrt(2), -45],
],
dtype=torch.float32,
device=device,
)
boxes2 = torch.tensor([[1, 1, 2, 2, 0]], dtype=torch.float32, device=device)
expected_ious = torch.tensor([[0.5], [0.5]], dtype=torch.float32, device=device)
ious = pairwise_iou(RotatedBoxes(boxes1), RotatedBoxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_pairwise_iou_orthogonal(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
boxes1 = torch.tensor([[5, 5, 10, 6, 55]], dtype=torch.float32, device=device)
boxes2 = torch.tensor([[5, 5, 10, 6, -35]], dtype=torch.float32, device=device)
iou = (6.0 * 6.0) / (6.0 * 6.0 + 4.0 * 6.0 + 4.0 * 6.0)
expected_ious = torch.tensor([[iou]], dtype=torch.float32, device=device)
ious = pairwise_iou(RotatedBoxes(boxes1), RotatedBoxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_pairwise_iou_large_close_boxes(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
boxes1 = torch.tensor(
[[299.500000, 417.370422, 600.000000, 364.259186, 27.1828]],
dtype=torch.float32,
device=device,
)
boxes2 = torch.tensor(
[[299.500000, 417.370422, 600.000000, 364.259155, 27.1828]],
dtype=torch.float32,
device=device,
)
iou = 364.259155 / 364.259186
expected_ious = torch.tensor([[iou]], dtype=torch.float32, device=device)
ious = pairwise_iou(RotatedBoxes(boxes1), RotatedBoxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_pairwise_iou_many_boxes(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
num_boxes1 = 100
num_boxes2 = 200
boxes1 = torch.stack(
[
torch.tensor(
[5 + 20 * i, 5 + 20 * i, 10, 10, 0], dtype=torch.float32, device=device
)
for i in range(num_boxes1)
]
)
boxes2 = torch.stack(
[
torch.tensor(
[5 + 20 * i, 5 + 20 * i, 10, 1 + 9 * i / num_boxes2, 0],
dtype=torch.float32,
device=device,
)
for i in range(num_boxes2)
]
)
expected_ious = torch.zeros(num_boxes1, num_boxes2, dtype=torch.float32, device=device)
for i in range(min(num_boxes1, num_boxes2)):
expected_ious[i][i] = (1 + 9 * i / num_boxes2) / 10.0
ious = pairwise_iou(RotatedBoxes(boxes1), RotatedBoxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_pairwise_iou_issue1207_simplified(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
# Simplified test case of D2-issue-1207
boxes1 = torch.tensor([[3, 3, 8, 2, -45.0]], device=device)
boxes2 = torch.tensor([[6, 0, 8, 2, -45.0]], device=device)
iou = 0.0
expected_ious = torch.tensor([[iou]], dtype=torch.float32, device=device)
ious = pairwise_iou(RotatedBoxes(boxes1), RotatedBoxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_pairwise_iou_issue1207(self):
for device in ["cpu"] + ["cuda"] if torch.cuda.is_available() else []:
# The original test case in D2-issue-1207
boxes1 = torch.tensor([[160.0, 153.0, 230.0, 23.0, -37.0]], device=device)
boxes2 = torch.tensor([[190.0, 127.0, 80.0, 21.0, -46.0]], device=device)
iou = 0.0
expected_ious = torch.tensor([[iou]], dtype=torch.float32, device=device)
ious = pairwise_iou(RotatedBoxes(boxes1), RotatedBoxes(boxes2))
self.assertTrue(torch.allclose(ious, expected_ious))
def test_empty_cat(self):
x = RotatedBoxes.cat([])
self.assertTrue(x.tensor.shape, (0, 5))
def benchmark_rotated_iou():
num_boxes1 = 200
num_boxes2 = 500
boxes1 = torch.stack(
[
torch.tensor([5 + 20 * i, 5 + 20 * i, 10, 10, 0], dtype=torch.float32)
for i in range(num_boxes1)
]
)
boxes2 = torch.stack(
[
torch.tensor(
[5 + 20 * i, 5 + 20 * i, 10, 1 + 9 * i / num_boxes2, 0], dtype=torch.float32
)
for i in range(num_boxes2)
]
)
def func(dev, n=1):
b1 = boxes1.to(device=dev)
b2 = boxes2.to(device=dev)
def bench():
for _ in range(n):
pairwise_iou_rotated(b1, b2)
if dev.type == "cuda":
torch.cuda.synchronize()
return bench
# only run it once per timed loop, since it's slow
args = [{"dev": torch.device("cpu"), "n": 1}]
if torch.cuda.is_available():
args.append({"dev": torch.device("cuda"), "n": 10})
benchmark(func, "rotated_iou", args, warmup_iters=3)
if __name__ == "__main__":
unittest.main()
benchmark_rotated_iou()

View File

@ -0,0 +1,48 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import unittest
from collections import OrderedDict
import torch
from torch import nn
from detectron2.checkpoint.c2_model_loading import align_and_update_state_dicts
from detectron2.utils.logger import setup_logger
class TestCheckpointer(unittest.TestCase):
def setUp(self):
setup_logger()
def create_complex_model(self):
m = nn.Module()
m.block1 = nn.Module()
m.block1.layer1 = nn.Linear(2, 3)
m.layer2 = nn.Linear(3, 2)
m.res = nn.Module()
m.res.layer2 = nn.Linear(3, 2)
state_dict = OrderedDict()
state_dict["layer1.weight"] = torch.rand(3, 2)
state_dict["layer1.bias"] = torch.rand(3)
state_dict["layer2.weight"] = torch.rand(2, 3)
state_dict["layer2.bias"] = torch.rand(2)
state_dict["res.layer2.weight"] = torch.rand(2, 3)
state_dict["res.layer2.bias"] = torch.rand(2)
return m, state_dict
def test_complex_model_loaded(self):
for add_data_parallel in [False, True]:
model, state_dict = self.create_complex_model()
if add_data_parallel:
model = nn.DataParallel(model)
model_sd = model.state_dict()
align_and_update_state_dicts(model_sd, state_dict)
for loaded, stored in zip(model_sd.values(), state_dict.values()):
# different tensor references
self.assertFalse(id(loaded) == id(stored))
# same content
self.assertTrue(loaded.equal(stored))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,240 @@
#!/usr/bin/env python
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import os
import tempfile
import unittest
import torch
from detectron2.config import configurable, downgrade_config, get_cfg, upgrade_config
from detectron2.layers import ShapeSpec
_V0_CFG = """
MODEL:
RPN_HEAD:
NAME: "TEST"
VERSION: 0
"""
_V1_CFG = """
MODEL:
WEIGHT: "/path/to/weight"
"""
class TestConfigVersioning(unittest.TestCase):
def test_upgrade_downgrade_consistency(self):
cfg = get_cfg()
# check that custom is preserved
cfg.USER_CUSTOM = 1
down = downgrade_config(cfg, to_version=0)
up = upgrade_config(down)
self.assertTrue(up == cfg)
def _merge_cfg_str(self, cfg, merge_str):
f = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False)
try:
f.write(merge_str)
f.close()
cfg.merge_from_file(f.name)
finally:
os.remove(f.name)
return cfg
def test_auto_upgrade(self):
cfg = get_cfg()
latest_ver = cfg.VERSION
cfg.USER_CUSTOM = 1
self._merge_cfg_str(cfg, _V0_CFG)
self.assertEqual(cfg.MODEL.RPN.HEAD_NAME, "TEST")
self.assertEqual(cfg.VERSION, latest_ver)
def test_guess_v1(self):
cfg = get_cfg()
latest_ver = cfg.VERSION
self._merge_cfg_str(cfg, _V1_CFG)
self.assertEqual(cfg.VERSION, latest_ver)
class _TestClassA(torch.nn.Module):
@configurable
def __init__(self, arg1, arg2, arg3=3):
super().__init__()
self.arg1 = arg1
self.arg2 = arg2
self.arg3 = arg3
assert arg1 == 1
assert arg2 == 2
assert arg3 == 3
@classmethod
def from_config(cls, cfg):
args = {"arg1": cfg.ARG1, "arg2": cfg.ARG2}
return args
class _TestClassB(_TestClassA):
@configurable
def __init__(self, input_shape, arg1, arg2, arg3=3):
"""
Doc of _TestClassB
"""
assert input_shape == "shape"
super().__init__(arg1, arg2, arg3)
@classmethod
def from_config(cls, cfg, input_shape): # test extra positional arg in from_config
args = {"arg1": cfg.ARG1, "arg2": cfg.ARG2}
args["input_shape"] = input_shape
return args
class _LegacySubClass(_TestClassB):
# an old subclass written in cfg style
def __init__(self, cfg, input_shape, arg4=4):
super().__init__(cfg, input_shape)
assert self.arg1 == 1
assert self.arg2 == 2
assert self.arg3 == 3
class _NewSubClassNewInit(_TestClassB):
# test new subclass with a new __init__
@configurable
def __init__(self, input_shape, arg4=4, **kwargs):
super().__init__(input_shape, **kwargs)
assert self.arg1 == 1
assert self.arg2 == 2
assert self.arg3 == 3
class _LegacySubClassNotCfg(_TestClassB):
# an old subclass written in cfg style, but argument is not called "cfg"
def __init__(self, config, input_shape):
super().__init__(config, input_shape)
assert self.arg1 == 1
assert self.arg2 == 2
assert self.arg3 == 3
class _TestClassC(_TestClassB):
@classmethod
def from_config(cls, cfg, input_shape, **kwargs): # test extra kwarg overwrite
args = {"arg1": cfg.ARG1, "arg2": cfg.ARG2}
args["input_shape"] = input_shape
args.update(kwargs)
return args
class _TestClassD(_TestClassA):
@configurable
def __init__(self, input_shape: ShapeSpec, arg1: int, arg2, arg3=3):
assert input_shape == "shape"
super().__init__(arg1, arg2, arg3)
# _TestClassA.from_config does not have input_shape args.
# Test whether input_shape will be forwarded to __init__
class TestConfigurable(unittest.TestCase):
def testInitWithArgs(self):
_ = _TestClassA(arg1=1, arg2=2, arg3=3)
_ = _TestClassB("shape", arg1=1, arg2=2)
_ = _TestClassC("shape", arg1=1, arg2=2)
_ = _TestClassD("shape", arg1=1, arg2=2, arg3=3)
def testPatchedAttr(self):
self.assertTrue("Doc" in _TestClassB.__init__.__doc__)
self.assertEqual(_TestClassD.__init__.__annotations__["arg1"], int)
def testInitWithCfg(self):
cfg = get_cfg()
cfg.ARG1 = 1
cfg.ARG2 = 2
cfg.ARG3 = 3
_ = _TestClassA(cfg)
_ = _TestClassB(cfg, input_shape="shape")
_ = _TestClassC(cfg, input_shape="shape")
_ = _TestClassD(cfg, input_shape="shape")
_ = _LegacySubClass(cfg, input_shape="shape")
_ = _NewSubClassNewInit(cfg, input_shape="shape")
_ = _LegacySubClassNotCfg(cfg, input_shape="shape")
with self.assertRaises(TypeError):
# disallow forwarding positional args to __init__ since it's prone to errors
_ = _TestClassD(cfg, "shape")
# call with kwargs instead
_ = _TestClassA(cfg=cfg)
_ = _TestClassB(cfg=cfg, input_shape="shape")
_ = _TestClassC(cfg=cfg, input_shape="shape")
_ = _TestClassD(cfg=cfg, input_shape="shape")
_ = _LegacySubClass(cfg=cfg, input_shape="shape")
_ = _NewSubClassNewInit(cfg=cfg, input_shape="shape")
_ = _LegacySubClassNotCfg(config=cfg, input_shape="shape")
def testInitWithCfgOverwrite(self):
cfg = get_cfg()
cfg.ARG1 = 1
cfg.ARG2 = 999 # wrong config
with self.assertRaises(AssertionError):
_ = _TestClassA(cfg, arg3=3)
# overwrite arg2 with correct config later:
_ = _TestClassA(cfg, arg2=2, arg3=3)
_ = _TestClassB(cfg, input_shape="shape", arg2=2, arg3=3)
_ = _TestClassC(cfg, input_shape="shape", arg2=2, arg3=3)
_ = _TestClassD(cfg, input_shape="shape", arg2=2, arg3=3)
# call with kwargs cfg=cfg instead
_ = _TestClassA(cfg=cfg, arg2=2, arg3=3)
_ = _TestClassB(cfg=cfg, input_shape="shape", arg2=2, arg3=3)
_ = _TestClassC(cfg=cfg, input_shape="shape", arg2=2, arg3=3)
_ = _TestClassD(cfg=cfg, input_shape="shape", arg2=2, arg3=3)
def testInitWithCfgWrongArgs(self):
cfg = get_cfg()
cfg.ARG1 = 1
cfg.ARG2 = 2
with self.assertRaises(TypeError):
_ = _TestClassB(cfg, "shape", not_exist=1)
with self.assertRaises(TypeError):
_ = _TestClassC(cfg, "shape", not_exist=1)
with self.assertRaises(TypeError):
_ = _TestClassD(cfg, "shape", not_exist=1)
def testBadClass(self):
class _BadClass1:
@configurable
def __init__(self, a=1, b=2):
pass
class _BadClass2:
@configurable
def __init__(self, a=1, b=2):
pass
def from_config(self, cfg): # noqa
pass
class _BadClass3:
@configurable
def __init__(self, a=1, b=2):
pass
# bad name: must be cfg
@classmethod
def from_config(cls, config): # noqa
pass
with self.assertRaises(AttributeError):
_ = _BadClass1(a=1)
with self.assertRaises(TypeError):
_ = _BadClass2(a=1)
with self.assertRaises(TypeError):
_ = _BadClass3(get_cfg())

View File

@ -0,0 +1,75 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import json
import os
import tempfile
import time
import unittest
from mock import MagicMock
import torch
from torch import nn
from detectron2.engine import SimpleTrainer, hooks
from detectron2.utils.events import CommonMetricPrinter, JSONWriter
class SimpleModel(nn.Module):
def __init__(self, sleep_sec=0):
super().__init__()
self.mod = nn.Linear(10, 20)
self.sleep_sec = sleep_sec
def forward(self, x):
if self.sleep_sec > 0:
time.sleep(self.sleep_sec)
return {"loss": x.sum() + sum([x.mean() for x in self.parameters()])}
class TestTrainer(unittest.TestCase):
def _data_loader(self, device):
device = torch.device(device)
while True:
yield torch.rand(3, 3).to(device)
def test_simple_trainer(self, device="cpu"):
model = SimpleModel().to(device=device)
trainer = SimpleTrainer(
model, self._data_loader(device), torch.optim.SGD(model.parameters(), 0.1)
)
trainer.train(0, 10)
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def test_simple_trainer_cuda(self):
self.test_simple_trainer(device="cuda")
def test_writer_hooks(self):
model = SimpleModel(sleep_sec=0.1)
trainer = SimpleTrainer(
model, self._data_loader("cpu"), torch.optim.SGD(model.parameters(), 0.1)
)
max_iter = 50
with tempfile.TemporaryDirectory(prefix="detectron2_test") as d:
json_file = os.path.join(d, "metrics.json")
writers = [CommonMetricPrinter(max_iter), JSONWriter(json_file)]
logger_info = writers[0].logger.info = MagicMock()
trainer.register_hooks(
[hooks.EvalHook(0, lambda: {"metric": 100}), hooks.PeriodicWriter(writers)]
)
trainer.train(0, max_iter)
with open(json_file, "r") as f:
data = [json.loads(line.strip()) for line in f]
self.assertEqual([x["iteration"] for x in data], [19, 39, 49, 50])
# the eval metric is in the last line with iter 50
self.assertIn("metric", data[-1], "Eval metric must be in last line of JSON!")
# test logged messages from CommonMetricPrinter
all_logs = [str(x) for x in logger_info.call_args_list]
self.assertEqual(len(all_logs), 3)
for log, iter in zip(all_logs, [19, 39, 49]):
self.assertIn(f"iter: {iter}", log)
self.assertIn("eta: 0:00:00", all_logs[-1], "Last ETA must be 0!")

View File

@ -0,0 +1,46 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import json
import os
import tempfile
import unittest
from detectron2.utils.events import EventStorage, JSONWriter
class TestEventWriter(unittest.TestCase):
def testScalar(self):
with tempfile.TemporaryDirectory(
prefix="detectron2_tests"
) as dir, EventStorage() as storage:
json_file = os.path.join(dir, "test.json")
writer = JSONWriter(json_file)
for k in range(60):
storage.put_scalar("key", k, smoothing_hint=False)
if (k + 1) % 20 == 0:
writer.write()
storage.step()
writer.close()
with open(json_file) as f:
data = [json.loads(l) for l in f]
self.assertTrue([int(k["key"]) for k in data] == [19, 39, 59])
def testScalarMismatchedPeriod(self):
with tempfile.TemporaryDirectory(
prefix="detectron2_tests"
) as dir, EventStorage() as storage:
json_file = os.path.join(dir, "test.json")
writer = JSONWriter(json_file)
for k in range(60):
if k % 17 == 0: # write in a differnt period
storage.put_scalar("key2", k, smoothing_hint=False)
storage.put_scalar("key", k, smoothing_hint=False)
if (k + 1) % 20 == 0:
writer.write()
storage.step()
writer.close()
with open(json_file) as f:
data = [json.loads(l) for l in f]
self.assertTrue([int(k.get("key2", 0)) for k in data] == [17, 0, 34, 0, 51, 0])
self.assertTrue([int(k.get("key", 0)) for k in data] == [0, 19, 0, 39, 0, 59])
self.assertTrue([int(k["iteration"]) for k in data] == [17, 19, 34, 39, 51, 59])

View File

@ -0,0 +1,70 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
# -*- coding: utf-8 -*-
import copy
import numpy as np
import os
import tempfile
import unittest
import cv2
import torch
from fvcore.common.file_io import PathManager
from detectron2 import model_zoo
from detectron2.checkpoint import DetectionCheckpointer
from detectron2.config import get_cfg
from detectron2.data import DatasetCatalog
from detectron2.modeling import build_model
from detectron2.utils.logger import setup_logger
@unittest.skipIf(os.environ.get("CIRCLECI"), "Require COCO data and model zoo.")
class TestCaffe2Export(unittest.TestCase):
def setUp(self):
setup_logger()
def _test_model(self, config_path, device="cpu"):
# requires extra dependencies
from detectron2.export import Caffe2Model, add_export_config, export_caffe2_model
cfg = get_cfg()
cfg.merge_from_file(model_zoo.get_config_file(config_path))
cfg = add_export_config(cfg)
cfg.MODEL.DEVICE = device
inputs = [{"image": self._get_test_image()}]
model = build_model(cfg)
DetectionCheckpointer(model).load(model_zoo.get_checkpoint_url(config_path))
c2_model = export_caffe2_model(cfg, model, copy.deepcopy(inputs))
with tempfile.TemporaryDirectory(prefix="detectron2_unittest") as d:
c2_model.save_protobuf(d)
c2_model.save_graph(os.path.join(d, "test.svg"), inputs=copy.deepcopy(inputs))
c2_model = Caffe2Model.load_protobuf(d)
c2_model(inputs)[0]["instances"]
def _get_test_image(self):
try:
file_name = DatasetCatalog.get("coco_2017_train")[0]["file_name"]
assert PathManager.exists(file_name)
except Exception:
self.skipTest("COCO dataset not available.")
with PathManager.open(file_name, "rb") as f:
buf = f.read()
img = cv2.imdecode(np.frombuffer(buf, dtype=np.uint8), cv2.IMREAD_COLOR)
assert img is not None, file_name
return torch.from_numpy(img.transpose(2, 0, 1))
def testMaskRCNN(self):
self._test_model("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml")
@unittest.skipIf(not torch.cuda.is_available(), "CUDA not available")
def testMaskRCNNGPU(self):
self._test_model("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml", device="cuda")
def testRetinaNet(self):
self._test_model("COCO-Detection/retinanet_R_50_FPN_3x.yaml")
def testPanopticFPN(self):
self._test_model("COCO-PanopticSegmentation/panoptic_fpn_R_50_3x.yaml")

View File

@ -0,0 +1,58 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
import unittest
import torch
import detectron2.model_zoo as model_zoo
from detectron2.config import get_cfg
from detectron2.modeling import build_model
from detectron2.utils.analysis import flop_count_operators, parameter_count
def get_model_zoo(config_path):
"""
Like model_zoo.get, but do not load any weights (even pretrained)
"""
cfg_file = model_zoo.get_config_file(config_path)
cfg = get_cfg()
cfg.merge_from_file(cfg_file)
if not torch.cuda.is_available():
cfg.MODEL.DEVICE = "cpu"
return build_model(cfg)
class RetinaNetTest(unittest.TestCase):
def setUp(self):
self.model = get_model_zoo("COCO-Detection/retinanet_R_50_FPN_1x.yaml")
def test_flop(self):
# RetinaNet supports flop-counting with random inputs
inputs = [{"image": torch.rand(3, 800, 800)}]
res = flop_count_operators(self.model, inputs)
self.assertTrue(int(res["conv"]), 146) # 146B flops
def test_param_count(self):
res = parameter_count(self.model)
self.assertTrue(res[""], 37915572)
self.assertTrue(res["backbone"], 31452352)
class FasterRCNNTest(unittest.TestCase):
def setUp(self):
self.model = get_model_zoo("COCO-Detection/faster_rcnn_R_50_FPN_1x.yaml")
def test_flop(self):
# Faster R-CNN supports flop-counting with random inputs
inputs = [{"image": torch.rand(3, 800, 800)}]
res = flop_count_operators(self.model, inputs)
# This only checks flops for backbone & proposal generator
# Flops for box head is not conv, and depends on #proposals, which is
# almost 0 for random inputs.
self.assertTrue(int(res["conv"]), 117)
def test_param_count(self):
res = parameter_count(self.model)
self.assertTrue(res[""], 41699936)
self.assertTrue(res["backbone"], 26799296)

View File

@ -0,0 +1,29 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import logging
import unittest
from detectron2 import model_zoo
from detectron2.modeling import FPN, GeneralizedRCNN
logger = logging.getLogger(__name__)
class TestModelZoo(unittest.TestCase):
def test_get_returns_model(self):
model = model_zoo.get("Misc/scratch_mask_rcnn_R_50_FPN_3x_gn.yaml", trained=False)
self.assertIsInstance(model, GeneralizedRCNN)
self.assertIsInstance(model.backbone, FPN)
def test_get_invalid_model(self):
self.assertRaises(RuntimeError, model_zoo.get, "Invalid/config.yaml")
def test_get_url(self):
url = model_zoo.get_checkpoint_url("Misc/scratch_mask_rcnn_R_50_FPN_3x_gn.yaml")
self.assertEqual(
url,
"https://dl.fbaipublicfiles.com/detectron2/Misc/scratch_mask_rcnn_R_50_FPN_3x_gn/138602908/model_final_01ca85.pkl", # noqa
)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,24 @@
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import unittest
from detectron2.utils.collect_env import collect_env_info
class TestProjects(unittest.TestCase):
def test_import(self):
from detectron2.projects import point_rend
_ = point_rend.add_pointrend_config
import detectron2.projects.deeplab as deeplab
_ = deeplab.add_deeplab_config
# import detectron2.projects.panoptic_deeplab as panoptic_deeplab
# _ = panoptic_deeplab.add_panoptic_deeplab_config
class TestCollectEnv(unittest.TestCase):
def test(self):
_ = collect_env_info()

View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import numpy as np
import os
import tempfile
import unittest
import cv2
import torch
from detectron2.data import MetadataCatalog
from detectron2.structures import BoxMode, Instances, RotatedBoxes
from detectron2.utils.visualizer import ColorMode, Visualizer
class TestVisualizer(unittest.TestCase):
def _random_data(self):
H, W = 100, 100
N = 10
img = np.random.rand(H, W, 3) * 255
boxxy = np.random.rand(N, 2) * (H // 2)
boxes = np.concatenate((boxxy, boxxy + H // 2), axis=1)
def _rand_poly():
return np.random.rand(3, 2).flatten() * H
polygons = [[_rand_poly() for _ in range(np.random.randint(1, 5))] for _ in range(N)]
mask = np.zeros_like(img[:, :, 0], dtype=np.bool)
mask[:40, 10:20] = 1
labels = [str(i) for i in range(N)]
return img, boxes, labels, polygons, [mask] * N
@property
def metadata(self):
return MetadataCatalog.get("coco_2017_train")
def test_draw_dataset_dict(self):
img = np.random.rand(512, 512, 3) * 255
dic = {
"annotations": [
{
"bbox": [
368.9946492271106,
330.891438763377,
13.148537455410235,
13.644708680142685,
],
"bbox_mode": BoxMode.XYWH_ABS,
"category_id": 0,
"iscrowd": 1,
"segmentation": {
"counts": "_jh52m?2N2N2N2O100O10O001N1O2MceP2",
"size": [512, 512],
},
}
],
"height": 512,
"image_id": 1,
"width": 512,
}
v = Visualizer(img, self.metadata)
v.draw_dataset_dict(dic)
def test_overlay_instances(self):
img, boxes, labels, polygons, masks = self._random_data()
v = Visualizer(img, self.metadata)
output = v.overlay_instances(masks=polygons, boxes=boxes, labels=labels).get_image()
self.assertEqual(output.shape, img.shape)
# Test 2x scaling
v = Visualizer(img, self.metadata, scale=2.0)
output = v.overlay_instances(masks=polygons, boxes=boxes, labels=labels).get_image()
self.assertEqual(output.shape[0], img.shape[0] * 2)
# Test overlay masks
v = Visualizer(img, self.metadata)
output = v.overlay_instances(masks=masks, boxes=boxes, labels=labels).get_image()
self.assertEqual(output.shape, img.shape)
def test_overlay_instances_no_boxes(self):
img, boxes, labels, polygons, _ = self._random_data()
v = Visualizer(img, self.metadata)
v.overlay_instances(masks=polygons, boxes=None, labels=labels).get_image()
def test_draw_instance_predictions(self):
img, boxes, _, _, masks = self._random_data()
num_inst = len(boxes)
inst = Instances((img.shape[0], img.shape[1]))
inst.pred_classes = torch.randint(0, 80, size=(num_inst,))
inst.scores = torch.rand(num_inst)
inst.pred_boxes = torch.from_numpy(boxes)
inst.pred_masks = torch.from_numpy(np.asarray(masks))
v = Visualizer(img, self.metadata)
v.draw_instance_predictions(inst)
def test_BWmode_nomask(self):
img, boxes, _, _, masks = self._random_data()
num_inst = len(boxes)
inst = Instances((img.shape[0], img.shape[1]))
inst.pred_classes = torch.randint(0, 80, size=(num_inst,))
inst.scores = torch.rand(num_inst)
inst.pred_boxes = torch.from_numpy(boxes)
v = Visualizer(img, self.metadata, instance_mode=ColorMode.IMAGE_BW)
v.draw_instance_predictions(inst)
def test_draw_empty_mask_predictions(self):
img, boxes, _, _, masks = self._random_data()
num_inst = len(boxes)
inst = Instances((img.shape[0], img.shape[1]))
inst.pred_classes = torch.randint(0, 80, size=(num_inst,))
inst.scores = torch.rand(num_inst)
inst.pred_boxes = torch.from_numpy(boxes)
inst.pred_masks = torch.from_numpy(np.zeros_like(np.asarray(masks)))
v = Visualizer(img, self.metadata)
v.draw_instance_predictions(inst)
def test_correct_output_shape(self):
img = np.random.rand(928, 928, 3) * 255
v = Visualizer(img, self.metadata)
out = v.output.get_image()
self.assertEqual(out.shape, img.shape)
def test_overlay_rotated_instances(self):
H, W = 100, 150
img = np.random.rand(H, W, 3) * 255
num_boxes = 50
boxes_5d = torch.zeros(num_boxes, 5)
boxes_5d[:, 0] = torch.FloatTensor(num_boxes).uniform_(-0.1 * W, 1.1 * W)
boxes_5d[:, 1] = torch.FloatTensor(num_boxes).uniform_(-0.1 * H, 1.1 * H)
boxes_5d[:, 2] = torch.FloatTensor(num_boxes).uniform_(0, max(W, H))
boxes_5d[:, 3] = torch.FloatTensor(num_boxes).uniform_(0, max(W, H))
boxes_5d[:, 4] = torch.FloatTensor(num_boxes).uniform_(-1800, 1800)
rotated_boxes = RotatedBoxes(boxes_5d)
labels = [str(i) for i in range(num_boxes)]
v = Visualizer(img, self.metadata)
output = v.overlay_instances(boxes=rotated_boxes, labels=labels).get_image()
self.assertEqual(output.shape, img.shape)
def test_draw_no_metadata(self):
img, boxes, _, _, masks = self._random_data()
num_inst = len(boxes)
inst = Instances((img.shape[0], img.shape[1]))
inst.pred_classes = torch.randint(0, 80, size=(num_inst,))
inst.scores = torch.rand(num_inst)
inst.pred_boxes = torch.from_numpy(boxes)
inst.pred_masks = torch.from_numpy(np.asarray(masks))
v = Visualizer(img, MetadataCatalog.get("asdfasdf"))
v.draw_instance_predictions(inst)
def test_draw_binary_mask(self):
img, boxes, _, _, masks = self._random_data()
img[:, :, 0] = 0 # remove red color
mask = masks[0]
mask_with_hole = np.zeros_like(mask).astype("uint8")
mask_with_hole = cv2.rectangle(mask_with_hole, (10, 10), (50, 50), 1, 5)
for m in [mask, mask_with_hole]:
for save in [True, False]:
v = Visualizer(img)
o = v.draw_binary_mask(m, color="red", text="test")
if save:
with tempfile.TemporaryDirectory(prefix="detectron2_viz") as d:
path = os.path.join(d, "output.png")
o.save(path)
o = cv2.imread(path)[:, :, ::-1]
else:
o = o.get_image().astype("float32")
# red color is drawn on the image
self.assertTrue(o[:, :, 0].sum() > 0)
def test_border(self):
H, W = 200, 200
img = np.zeros((H, W, 3))
img[:, :, 0] = 255.0
v = Visualizer(img, scale=3)
mask = np.zeros((H, W))
mask[:, 100:150] = 1
# create a hole, to trigger imshow
mask = cv2.rectangle(mask, (110, 110), (130, 130), 0, thickness=-1)
output = v.draw_binary_mask(mask, color="blue")
output = output.get_image()[:, :, ::-1]
first_row = {tuple(x.tolist()) for x in output[0]}
last_row = {tuple(x.tolist()) for x in output[-1]}
# check quantization / off-by-1 error: the first and last row must have two colors
self.assertEqual(len(last_row), 2)
self.assertEqual(len(first_row), 2)
self.assertIn((0, 0, 255), last_row)
self.assertIn((0, 0, 255), first_row)
if __name__ == "__main__":
unittest.main()