Files
paddlepaddle--paddle/test/legacy_test/test_generate_proposals_v2_op.py
T
2026-07-13 12:40:42 +08:00

655 lines
22 KiB
Python

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import math
import unittest
import numpy as np
from op_test import OpTest
from test_anchor_generator_op import anchor_generator_in_python
import paddle
def box_coder(all_anchors, bbox_deltas, variances, pixel_offset=True):
"""
Decode proposals by anchors and bbox_deltas from RPN
"""
offset = 1 if pixel_offset else 0
# proposals: xmin, ymin, xmax, ymax
proposals = np.zeros_like(bbox_deltas, dtype=np.float32)
# anchor_loc: width, height, center_x, center_y
anchor_loc = np.zeros_like(bbox_deltas, dtype=np.float32)
anchor_loc[:, 0] = all_anchors[:, 2] - all_anchors[:, 0] + offset
anchor_loc[:, 1] = all_anchors[:, 3] - all_anchors[:, 1] + offset
anchor_loc[:, 2] = all_anchors[:, 0] + 0.5 * anchor_loc[:, 0]
anchor_loc[:, 3] = all_anchors[:, 1] + 0.5 * anchor_loc[:, 1]
# predicted bbox: bbox_center_x, bbox_center_y, bbox_width, bbox_height
pred_bbox = np.zeros_like(bbox_deltas, dtype=np.float32)
if variances is not None:
for i in range(bbox_deltas.shape[0]):
pred_bbox[i, 0] = (
variances[i, 0] * bbox_deltas[i, 0] * anchor_loc[i, 0]
+ anchor_loc[i, 2]
)
pred_bbox[i, 1] = (
variances[i, 1] * bbox_deltas[i, 1] * anchor_loc[i, 1]
+ anchor_loc[i, 3]
)
pred_bbox[i, 2] = (
math.exp(
min(
variances[i, 2] * bbox_deltas[i, 2],
math.log(1000 / 16.0),
)
)
* anchor_loc[i, 0]
)
pred_bbox[i, 3] = (
math.exp(
min(
variances[i, 3] * bbox_deltas[i, 3],
math.log(1000 / 16.0),
)
)
* anchor_loc[i, 1]
)
else:
for i in range(bbox_deltas.shape[0]):
pred_bbox[i, 0] = (
bbox_deltas[i, 0] * anchor_loc[i, 0] + anchor_loc[i, 2]
)
pred_bbox[i, 1] = (
bbox_deltas[i, 1] * anchor_loc[i, 1] + anchor_loc[i, 3]
)
pred_bbox[i, 2] = (
math.exp(min(bbox_deltas[i, 2], math.log(1000 / 16.0)))
* anchor_loc[i, 0]
)
pred_bbox[i, 3] = (
math.exp(min(bbox_deltas[i, 3], math.log(1000 / 16.0)))
* anchor_loc[i, 1]
)
proposals[:, 0] = pred_bbox[:, 0] - pred_bbox[:, 2] / 2
proposals[:, 1] = pred_bbox[:, 1] - pred_bbox[:, 3] / 2
proposals[:, 2] = pred_bbox[:, 0] + pred_bbox[:, 2] / 2 - offset
proposals[:, 3] = pred_bbox[:, 1] + pred_bbox[:, 3] / 2 - offset
return proposals
def clip_tiled_boxes(boxes, im_shape, pixel_offset=True):
"""Clip boxes to image boundaries. im_shape is [height, width] and boxes
has shape (N, 4 * num_tiled_boxes)."""
assert boxes.shape[1] % 4 == 0, (
f'boxes.shape[1] is {boxes.shape[1]:d}, but must be divisible by 4.'
)
offset = 1 if pixel_offset else 0
# x1 >= 0
boxes[:, 0::4] = np.maximum(
np.minimum(boxes[:, 0::4], im_shape[1] - offset), 0
)
# y1 >= 0
boxes[:, 1::4] = np.maximum(
np.minimum(boxes[:, 1::4], im_shape[0] - offset), 0
)
# x2 < im_shape[1]
boxes[:, 2::4] = np.maximum(
np.minimum(boxes[:, 2::4], im_shape[1] - offset), 0
)
# y2 < im_shape[0]
boxes[:, 3::4] = np.maximum(
np.minimum(boxes[:, 3::4], im_shape[0] - offset), 0
)
return boxes
def iou(box_a, box_b, pixel_offset=True):
"""
Apply intersection-over-union overlap between box_a and box_b
"""
xmin_a = min(box_a[0], box_a[2])
ymin_a = min(box_a[1], box_a[3])
xmax_a = max(box_a[0], box_a[2])
ymax_a = max(box_a[1], box_a[3])
xmin_b = min(box_b[0], box_b[2])
ymin_b = min(box_b[1], box_b[3])
xmax_b = max(box_b[0], box_b[2])
ymax_b = max(box_b[1], box_b[3])
offset = 1 if pixel_offset else 0
area_a = (ymax_a - ymin_a + offset) * (xmax_a - xmin_a + offset)
area_b = (ymax_b - ymin_b + offset) * (xmax_b - xmin_b + offset)
if area_a <= 0 and area_b <= 0:
return 0.0
xa = max(xmin_a, xmin_b)
ya = max(ymin_a, ymin_b)
xb = min(xmax_a, xmax_b)
yb = min(ymax_a, ymax_b)
inter_area = max(xb - xa + offset, 0.0) * max(yb - ya + offset, 0.0)
iou_ratio = inter_area / (area_a + area_b - inter_area)
return iou_ratio
def nms(boxes, scores, nms_threshold, eta=1.0, pixel_offset=True):
"""Apply non-maximum suppression at test time to avoid detecting too many
overlapping bounding boxes for a given object.
Args:
boxes: (tensor) The location preds for the img, Shape: [num_priors,4].
scores: (tensor) The class predscores for the img, Shape:[num_priors].
nms_threshold: (float) The overlap thresh for suppressing unnecessary
boxes.
eta: (float) The parameter for adaptive NMS.
Return:
The indices of the kept boxes with respect to num_priors.
"""
all_scores = copy.deepcopy(scores)
all_scores = all_scores.flatten()
sorted_indices = np.argsort(-all_scores, axis=0, kind='mergesort')
sorted_scores = all_scores[sorted_indices]
selected_indices = []
adaptive_threshold = nms_threshold
for i in range(sorted_scores.shape[0]):
idx = sorted_indices[i]
keep = True
for k in range(len(selected_indices)):
if keep:
kept_idx = selected_indices[k]
overlap = iou(
boxes[idx], boxes[kept_idx], pixel_offset=pixel_offset
)
keep = True if overlap <= adaptive_threshold else False
else:
break
if keep:
selected_indices.append(idx)
if keep and eta < 1 and adaptive_threshold > 0.5:
adaptive_threshold *= eta
return selected_indices
def python_generate_proposals_v2(
scores,
bbox_deltas,
img_size,
anchors,
variances,
pre_nms_top_n=6000,
post_nms_top_n=1000,
nms_thresh=0.5,
min_size=0.1,
eta=1.0,
pixel_offset=False,
return_rois_num=True,
):
(
rpn_rois,
rpn_roi_probs,
rpn_rois_num,
) = paddle.vision.ops.generate_proposals(
scores,
bbox_deltas,
img_size,
anchors,
variances,
pre_nms_top_n=pre_nms_top_n,
post_nms_top_n=post_nms_top_n,
nms_thresh=nms_thresh,
min_size=min_size,
eta=eta,
pixel_offset=pixel_offset,
return_rois_num=return_rois_num,
)
return rpn_rois, rpn_roi_probs, rpn_rois_num
def generate_proposals_v2_in_python(
scores,
bbox_deltas,
im_shape,
anchors,
variances,
pre_nms_topN,
post_nms_topN,
nms_thresh,
min_size,
eta,
pixel_offset,
):
all_anchors = anchors.reshape(-1, 4)
rois = np.empty((0, 5), dtype=np.float32)
roi_probs = np.empty((0, 1), dtype=np.float32)
rpn_rois = []
rpn_roi_probs = []
rois_num = []
num_images = scores.shape[0]
for img_idx in range(num_images):
img_i_boxes, img_i_probs = proposal_for_one_image(
im_shape[img_idx, :],
all_anchors,
variances,
bbox_deltas[img_idx, :, :, :],
scores[img_idx, :, :, :],
pre_nms_topN,
post_nms_topN,
nms_thresh,
min_size,
eta,
pixel_offset,
)
rois_num.append(img_i_probs.shape[0])
rpn_rois.append(img_i_boxes)
rpn_roi_probs.append(img_i_probs)
return rpn_rois, rpn_roi_probs, rois_num
def proposal_for_one_image(
im_shape,
all_anchors,
variances,
bbox_deltas,
scores,
pre_nms_topN,
post_nms_topN,
nms_thresh,
min_size,
eta,
pixel_offset,
):
# Transpose and reshape predicted bbox transformations to get them
# into the same order as the anchors:
# - bbox deltas will be (4 * A, H, W) format from conv output
# - transpose to (H, W, 4 * A)
# - reshape to (H * W * A, 4) where rows are ordered by (H, W, A)
# in slowest to fastest order to match the enumerated anchors
bbox_deltas = bbox_deltas.transpose((1, 2, 0)).reshape(-1, 4)
all_anchors = all_anchors.reshape(-1, 4)
variances = variances.reshape(-1, 4)
# Same story for the scores:
# - scores are (A, H, W) format from conv output
# - transpose to (H, W, A)
# - reshape to (H * W * A, 1) where rows are ordered by (H, W, A)
# to match the order of anchors and bbox_deltas
scores = scores.transpose((1, 2, 0)).reshape(-1, 1)
# sort all (proposal, score) pairs by score from highest to lowest
# take top pre_nms_topN (e.g. 6000)
if pre_nms_topN <= 0 or pre_nms_topN >= len(scores):
order = np.argsort(-scores.squeeze())
else:
# Avoid sorting possibly large arrays;
# First partition to get top K unsorted
# and then sort just those
inds = np.argpartition(-scores.squeeze(), pre_nms_topN)[:pre_nms_topN]
order = np.argsort(-scores[inds].squeeze())
order = inds[order]
scores = scores[order, :]
bbox_deltas = bbox_deltas[order, :]
all_anchors = all_anchors[order, :]
proposals = box_coder(all_anchors, bbox_deltas, variances, pixel_offset)
# clip proposals to image (may result in proposals with zero area
# that will be removed in the next step)
proposals = clip_tiled_boxes(proposals, im_shape, pixel_offset)
# remove predicted boxes with height or width < min_size
keep = filter_boxes(proposals, min_size, im_shape, pixel_offset)
if len(keep) == 0:
proposals = np.zeros((1, 4)).astype('float32')
scores = np.zeros((1, 1)).astype('float32')
return proposals, scores
proposals = proposals[keep, :]
scores = scores[keep, :]
# apply loose nms (e.g. threshold = 0.7)
# take post_nms_topN (e.g. 1000)
# return the top proposals
if nms_thresh > 0:
keep = nms(
boxes=proposals,
scores=scores,
nms_threshold=nms_thresh,
eta=eta,
pixel_offset=pixel_offset,
)
if post_nms_topN > 0 and post_nms_topN < len(keep):
keep = keep[:post_nms_topN]
proposals = proposals[keep, :]
scores = scores[keep, :]
return proposals, scores
def filter_boxes(boxes, min_size, im_shape, pixel_offset=True):
"""Only keep boxes with both sides >= min_size and center within the image."""
# Scale min_size to match image scale
min_size = max(min_size, 1.0)
offset = 1 if pixel_offset else 0
ws = boxes[:, 2] - boxes[:, 0] + offset
hs = boxes[:, 3] - boxes[:, 1] + offset
if pixel_offset:
x_ctr = boxes[:, 0] + ws / 2.0
y_ctr = boxes[:, 1] + hs / 2.0
keep = np.where(
(ws >= min_size)
& (hs >= min_size)
& (x_ctr < im_shape[1])
& (y_ctr < im_shape[0])
)[0]
else:
keep = np.where((ws >= min_size) & (hs >= min_size))[0]
return keep
class TestGenerateProposalsV2Op(OpTest):
def set_data(self):
self.init_test_params()
self.init_test_input()
self.init_test_output()
self.inputs = {
'Scores': self.scores,
'BboxDeltas': self.bbox_deltas,
'ImShape': self.im_shape.astype(np.float32),
'Anchors': self.anchors,
'Variances': self.variances,
}
self.attrs = {
'pre_nms_topN': self.pre_nms_topN,
'post_nms_topN': self.post_nms_topN,
'nms_thresh': self.nms_thresh,
'min_size': self.min_size,
'eta': self.eta,
'pixel_offset': self.pixel_offset,
}
self.outputs = {
'RpnRois': self.rpn_rois[0],
'RpnRoiProbs': self.rpn_roi_probs[0],
}
def test_check_output(self):
self.check_output(check_pir=True, check_symbol_infer=False)
def setUp(self):
self.op_type = "generate_proposals_v2"
self.python_api = python_generate_proposals_v2
self.set_data()
def init_test_params(self):
self.pre_nms_topN = 12000 # train 12000, test 2000
self.post_nms_topN = 5000 # train 6000, test 1000
self.nms_thresh = 0.7
self.min_size = 3.0
self.eta = 1.0
self.pixel_offset = True
def init_test_input(self):
batch_size = 1
input_channels = 20
layer_h = 16
layer_w = 16
input_feat = np.random.random(
(batch_size, input_channels, layer_h, layer_w)
).astype('float32')
self.anchors, self.variances = anchor_generator_in_python(
input_feat=input_feat,
anchor_sizes=[16.0, 32.0],
aspect_ratios=[0.5, 1.0],
variances=[1.0, 1.0, 1.0, 1.0],
stride=[16.0, 16.0],
offset=0.5,
)
self.im_shape = np.array([[64, 64]]).astype('float32')
num_anchors = self.anchors.shape[2]
self.scores = np.random.random(
(batch_size, num_anchors, layer_h, layer_w)
).astype('float32')
self.bbox_deltas = np.random.random(
(batch_size, num_anchors * 4, layer_h, layer_w)
).astype('float32')
def init_test_output(self):
(
self.rpn_rois,
self.rpn_roi_probs,
self.rois_num,
) = generate_proposals_v2_in_python(
self.scores,
self.bbox_deltas,
self.im_shape,
self.anchors,
self.variances,
self.pre_nms_topN,
self.post_nms_topN,
self.nms_thresh,
self.min_size,
self.eta,
self.pixel_offset,
)
# class TestGenerateProposalsV2OpNoBoxLeft(TestGenerateProposalsV2Op):
# def init_test_params(self):
# self.pre_nms_topN = 12000 # train 12000, test 2000
# self.post_nms_topN = 5000 # train 6000, test 1000
# self.nms_thresh = 0.7
# self.min_size = 1000.0
# self.eta = 1.
# self.pixel_offset = True
# class TestGenerateProposalsV2OpNoOffset(TestGenerateProposalsV2Op):
# def init_test_params(self):
# self.pre_nms_topN = 12000 # train 12000, test 2000
# self.post_nms_topN = 5000 # train 6000, test 1000
# self.nms_thresh = 0.7
# self.min_size = 3.0
# self.eta = 1.
# self.pixel_offset = False
# class testGenerateProposalsAPI(unittest.TestCase):
# def setUp(self):
# np.random.seed(678)
# self.scores_np = np.random.rand(2, 3, 4, 4).astype('float32')
# self.bbox_deltas_np = np.random.rand(2, 12, 4, 4).astype('float32')
# self.img_size_np = np.array([[8, 8], [6, 6]]).astype('float32')
# self.anchors_np = np.reshape(np.arange(4 * 4 * 3 * 4),
# [4, 4, 3, 4]).astype('float32')
# self.variances_np = np.ones((4, 4, 3, 4)).astype('float32')
# self.roi_expected, self.roi_probs_expected, self.rois_num_expected = generate_proposals_v2_in_python(
# self.scores_np,
# self.bbox_deltas_np,
# self.img_size_np,
# self.anchors_np,
# self.variances_np,
# pre_nms_topN=10,
# post_nms_topN=5,
# nms_thresh=0.5,
# min_size=0.1,
# eta=1.0,
# pixel_offset=False)
# self.roi_expected = np.array(self.roi_expected).squeeze(1)
# self.roi_probs_expected = np.array(self.roi_probs_expected).squeeze(1)
# self.rois_num_expected = np.array(self.rois_num_expected)
# def test_dynamic(self):
# paddle.disable_static()
# scores = paddle.to_tensor(self.scores_np)
# bbox_deltas = paddle.to_tensor(self.bbox_deltas_np)
# img_size = paddle.to_tensor(self.img_size_np)
# anchors = paddle.to_tensor(self.anchors_np)
# variances = paddle.to_tensor(self.variances_np)
# rois, roi_probs, rois_num = paddle.vision.ops.generate_proposals(
# scores,
# bbox_deltas,
# img_size,
# anchors,
# variances,
# pre_nms_top_n=10,
# post_nms_top_n=5,
# return_rois_num=True)
# np.testing.assert_allclose(self.roi_expected, rois.numpy(), rtol=1e-5)
# np.testing.assert_allclose(self.roi_probs_expected, roi_probs.numpy(), rtol=1e-5)
# np.testing.assert_allclose(self.rois_num_expected, rois_num.numpy(), rtol=1e-5)
# def test_static(self):
# paddle.enable_static()
# scores = paddle.static.data(name='scores',
# shape=[2, 3, 4, 4],
# dtype='float32')
# bbox_deltas = paddle.static.data(name='bbox_deltas',
# shape=[2, 12, 4, 4],
# dtype='float32')
# img_size = paddle.static.data(name='img_size',
# shape=[2, 2],
# dtype='float32')
# anchors = paddle.static.data(name='anchors',
# shape=[4, 4, 3, 4],
# dtype='float32')
# variances = paddle.static.data(name='variances',
# shape=[4, 4, 3, 4],
# dtype='float32')
# rois, roi_probs, rois_num = paddle.vision.ops.generate_proposals(
# scores,
# bbox_deltas,
# img_size,
# anchors,
# variances,
# pre_nms_top_n=10,
# post_nms_top_n=5,
# return_rois_num=True)
# exe = paddle.static.Executor()
# rois, roi_probs, rois_num = exe.run(
# paddle.static.default_main_program(),
# feed={
# 'scores': self.scores_np,
# 'bbox_deltas': self.bbox_deltas_np,
# 'img_size': self.img_size_np,
# 'anchors': self.anchors_np,
# 'variances': self.variances_np,
# },
# fetch_list=[rois.name, roi_probs.name, rois_num.name],
# return_numpy=False)
# np.testing.assert_allclose(self.roi_expected, np.array(rois), rtol=1e-5)
# np.testing.assert_allclose(self.roi_probs_expected, np.array(roi_probs), rtol=1e-5)
# np.testing.assert_allclose(self.rois_num_expected, np.array(rois_num), rtol=1e-5)
class TestGenerateProposalsV2Op_ZeroSize(OpTest):
def set_data(self):
self.init_test_params()
self.init_test_input()
self.init_test_output()
self.inputs = {
'Scores': self.scores,
'BboxDeltas': self.bbox_deltas,
'ImShape': self.im_shape.astype(np.float32),
'Anchors': self.anchors,
'Variances': self.variances,
}
self.attrs = {
'pre_nms_topN': self.pre_nms_topN,
'post_nms_topN': self.post_nms_topN,
'nms_thresh': self.nms_thresh,
'min_size': self.min_size,
'eta': self.eta,
'pixel_offset': self.pixel_offset,
}
self.outputs = {
'RpnRois': self.rpn_rois[0],
'RpnRoiProbs': self.rpn_roi_probs[0],
}
def test_check_output(self):
self.check_output(check_pir=True, check_symbol_infer=False)
def setUp(self):
self.op_type = "generate_proposals_v2"
self.python_api = python_generate_proposals_v2
self.set_data()
def init_test_params(self):
self.pre_nms_topN = 12000 # train 12000, test 2000
self.post_nms_topN = 5000 # train 6000, test 1000
self.nms_thresh = 0.7
self.min_size = 3.0
self.eta = 1.0
self.pixel_offset = True
def init_test_input(self):
batch_size = 1
input_channels = 0
layer_h = 16
layer_w = 16
input_feat = np.random.random(
(batch_size, input_channels, layer_h, layer_w)
).astype('float32')
self.anchors, self.variances = anchor_generator_in_python(
input_feat=input_feat,
anchor_sizes=[16.0, 32.0],
aspect_ratios=[0.5, 1.0],
variances=[1.0, 1.0, 1.0, 1.0],
stride=[16.0, 16.0],
offset=0.5,
)
self.im_shape = np.array([[64, 64]]).astype('float32')
num_anchors = self.anchors.shape[2]
self.scores = np.random.random(
(batch_size, num_anchors, layer_h, layer_w)
).astype('float32')
self.bbox_deltas = np.random.random(
(batch_size, num_anchors * 4, layer_h, layer_w)
).astype('float32')
def init_test_output(self):
(
self.rpn_rois,
self.rpn_roi_probs,
self.rois_num,
) = generate_proposals_v2_in_python(
self.scores,
self.bbox_deltas,
self.im_shape,
self.anchors,
self.variances,
self.pre_nms_topN,
self.post_nms_topN,
self.nms_thresh,
self.min_size,
self.eta,
self.pixel_offset,
)
if __name__ == '__main__':
paddle.enable_static()
unittest.main()