Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Object Detection] Gluoncv SSD support on CPU #2353

Merged
merged 43 commits into from
Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1da5012
Refactor NMS
Jul 29, 2018
1eb27a8
Avoid using function call for hybrid frontend
Sep 4, 2018
1e2cdb5
Modify nms
Nov 28, 2018
d892867
Add box_nms
Dec 11, 2018
c19526b
Add test for get_valid_counts
Dec 13, 2018
9e0eee7
Add missing operators
Dec 16, 2018
037ca23
Add id_index to box_nms op
Dec 16, 2018
8f9e9e2
Add l2_normalize to from_mxnet
Dec 16, 2018
5c5e6f7
Modify SSD tutorial
Dec 19, 2018
3054af5
Fix tutorial
Dec 19, 2018
ef21b02
Relay support
Jan 8, 2019
31f0927
Add more ops to from_mxnet
Jan 8, 2019
fb43612
Support multibox op with hybrid script
Jan 11, 2019
717e61c
Fix slice_axis relay register issue
Jan 12, 2019
b9681ee
Add get_valid_counts relay test
Jan 13, 2019
e277a55
Fix multibox_transform_loc
Jan 14, 2019
ef00b7f
Fix relay from_mxnet
Jan 14, 2019
fa89a2a
Fix l2_normalize
Jan 14, 2019
925c140
Fix lint
Jan 15, 2019
6819dc3
Add cuda schedule for get_valid_counts
Jan 15, 2019
8eaff5c
Fix tutorial
Jan 15, 2019
7423762
Fix ctx_list
Jan 18, 2019
b3c8a7c
Add install gluoncv
Jan 18, 2019
986c4f7
Disable box_nms in frontend test
Jan 28, 2019
e7df94c
Fix test get_valid_counts numpy result
Jan 28, 2019
45b6aac
Rename ssd tutorial
Jan 31, 2019
26ece34
Fix rebase
Jan 31, 2019
2f8aef8
Refactor nms
Feb 19, 2019
d30be86
Rollback nnvm
Feb 19, 2019
6b1fd7a
Refactor relay nms
Feb 20, 2019
42571cf
Add max_output_size arg
Feb 22, 2019
f8fecec
Make return_indices optional
Feb 24, 2019
11c8bba
Minor fix
Feb 24, 2019
908eedb
Resolve conflict
Feb 24, 2019
9743f15
Rename topk to top_k
Feb 25, 2019
4bd6fec
Fix example code
Feb 27, 2019
d1e95f9
Fix lint
Mar 3, 2019
292130d
Minor fix
Mar 3, 2019
fb94ffe
Remove contrib_slice_axis
Mar 4, 2019
5c0cee9
Resolve conflict
Mar 4, 2019
28d479a
Address minor comments
Mar 5, 2019
c2e02e4
Move tutorial
Mar 7, 2019
d20024c
Resolve conflict
Mar 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions include/tvm/relay/attrs/vision.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,42 @@ struct MultiBoxTransformLocAttrs
}
};

/*! \brief Attributes used in non_maximum_suppression operators */
struct NMSAttrs : public tvm::AttrsNode<NMSAttrs>{
double overlap_threshold;
/*! \brief Attributes used in get_valid_counts operator */
struct GetValidCountsAttrs : public tvm::AttrsNode<GetValidCountsAttrs> {
double score_threshold;

TVM_DECLARE_ATTRS(GetValidCountsAttrs, "relay.attrs.GetValidCountsAttrs") {
TVM_ATTR_FIELD(score_threshold).set_default(0.0)
.describe("Lower limit of score for valid bounding boxes.");
}
};

/*! \brief Attributes used in non_maximum_suppression operator */
struct NonMaximumSuppressionAttrs : public tvm::AttrsNode<NonMaximumSuppressionAttrs> {
int max_output_size;
double iou_threshold;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us deliberate on the choice of the parameter name. In particular why the name

  • iou_threshold, id_index(while it refers to axis?), do_rearrange

Copy link
Contributor Author

@kevinthesun kevinthesun Feb 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iou_threshold is the threshold of intersection over union(IoU). This is different from mxnet API, but I think it might be easier to understand in object detection context? @zhreshold

You are right. id_index in fact means axis. I think we can change it to id_axis.

do_rearrange is whether to move all invalid boxed to the bottom. Maybe we can change it to invalid_to_bottom to be more meaningful?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have a strong opinion on the naming choices, but perhaps it would be helpful to bring several people who are involved. Mainly because API relates to backward compatibility and we want to be careful. How about a quick RFC?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool force_suppress;
int topk;

TVM_DECLARE_ATTRS(NMSAttrs, "relay.attrs.NMSAttrs") {
TVM_ATTR_FIELD(overlap_threshold).set_default(0.5)
.describe("Non-maximum suppression threshold.");
TVM_ATTR_FIELD(force_suppress).set_default(false)
.describe("Suppress all detections regardless of class_id.");
TVM_ATTR_FIELD(topk).set_default(-1)
.describe("Keep maximum top k detections before nms, -1 for no limit.");
int top_k;
int id_index;
bool return_indices;
bool invalid_to_bottom;

TVM_DECLARE_ATTRS(NonMaximumSuppressionAttrs, "relay.attrs.NonMaximumSuppressionAttrs") {
TVM_ATTR_FIELD(max_output_size).set_default(-1)
.describe("Max number of output valid boxes for each instance."
"By default all valid boxes are returned.");
TVM_ATTR_FIELD(iou_threshold).set_default(0.5)
.describe("Non-maximum suppression threshold.");
TVM_ATTR_FIELD(force_suppress).set_default(false)
.describe("Suppress all detections regardless of class_id.");
TVM_ATTR_FIELD(top_k).set_default(-1)
.describe("Keep maximum top k detections before nms, -1 for no limit.");
TVM_ATTR_FIELD(id_index).set_default(0)
.describe("Axis index of id.");
TVM_ATTR_FIELD(return_indices).set_default(true)
.describe("Whether to return box indices in input data.");
TVM_ATTR_FIELD(invalid_to_bottom).set_default(false)
.describe("Whether to move all invalid bounding boxes to the bottom.");
}
};

Expand Down
29 changes: 21 additions & 8 deletions nnvm/include/nnvm/top/nn.h
Original file line number Diff line number Diff line change
Expand Up @@ -443,17 +443,30 @@ struct MultiBoxTransformLocParam : public dmlc::Parameter<MultiBoxTransformLocPa
}
};

struct NMSParam : public dmlc::Parameter<NMSParam> {
float nms_threshold;
struct NonMaximumSuppressionParam : public dmlc::Parameter<NonMaximumSuppressionParam> {
bool return_indices;
float iou_threshold;
bool force_suppress;
int nms_topk;
DMLC_DECLARE_PARAMETER(NMSParam) {
DMLC_DECLARE_FIELD(nms_threshold).set_default(0.5)
int top_k;
int id_index;
int max_output_size;
bool invalid_to_bottom;
DMLC_DECLARE_PARAMETER(NonMaximumSuppressionParam) {
DMLC_DECLARE_FIELD(max_output_size).set_default(-1)
.describe("Max number of output valid boxes for each instance."
"By default all valid boxes are returned.");
DMLC_DECLARE_FIELD(iou_threshold).set_default(0.5)
.describe("Non-maximum suppression threshold.");
DMLC_DECLARE_FIELD(force_suppress).set_default(false)
.describe("Suppress all detections regardless of class_id.");
DMLC_DECLARE_FIELD(nms_topk).set_default(-1)
.describe("Keep maximum top k detections before nms, -1 for no limit.");
.describe("Suppress all detections regardless of class_id.");
DMLC_DECLARE_FIELD(top_k).set_default(-1)
.describe("Keep maximum top k detections before nms, -1 for no limit.");
DMLC_DECLARE_FIELD(id_index).set_default(0)
.describe("Axis index of id.");
DMLC_DECLARE_FIELD(return_indices).set_default(true)
.describe("Whether to return box indices in input data.");
DMLC_DECLARE_FIELD(invalid_to_bottom).set_default(false)
.describe("Whether to move all invalid bounding boxes to the bottom.");
}
};

Expand Down
6 changes: 3 additions & 3 deletions nnvm/python/nnvm/frontend/mxnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,11 @@ def _contrib_multibox_detection(inputs, attrs):
if attrs.get('variances') is not None else (0.1, 0.1, 0.2, 0.2)
nms_topk = attrs.get('nms_topk') or -1
new_attrs0 = {'clip': clip, 'threshold': float(threshold), 'variances': variances}
new_attrs1 = {'nms_threshold': float(nms_threshold), 'force_suppress': force_suppress,
'nms_topk': int(nms_topk)}
new_attrs1 = {'return_indices': False, 'iou_threshold': float(nms_threshold),
'force_suppress': force_suppress, 'top_k': int(nms_topk)}
data, valid_count = _get_nnvm_op('multibox_transform_loc')(inputs[0], inputs[1],
inputs[2], **new_attrs0)
return _get_nnvm_op('nms')(data, valid_count, **new_attrs1)
return _get_nnvm_op('non_max_suppression')(data, valid_count, **new_attrs1)

def _elemwise_sum(inputs, _):
new_attrs = {'num_args':len(inputs)}
Expand Down
23 changes: 14 additions & 9 deletions nnvm/python/nnvm/top/vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,25 @@ def compute_multibox_transform_loc(attrs, inputs, _):
reg.register_pattern("multibox_detection", OpPattern.OPAQUE)

# non-maximum suppression
@reg.register_schedule("nms")
@reg.register_schedule("non_max_suppression")
def schedule_nms(_, outs, target):
"""Schedule definition of nms"""
"""Schedule definition of non_max_suppression"""
with tvm.target.create(target):
return topi.generic.schedule_nms(outs)

@reg.register_compute("nms")
@reg.register_compute("non_max_suppression")
def compute_nms(attrs, inputs, _):
"""Compute definition of nms"""
nms_threshold = attrs.get_float('nms_threshold')
"""Compute definition of non_max_suppression"""
return_indices = attrs.get_bool('return_indices')
max_output_size = attrs.get_int('max_output_size')
iou_threshold = attrs.get_float('iou_threshold')
force_suppress = attrs.get_bool('force_suppress')
nms_topk = attrs.get_int('nms_topk')
top_k = attrs.get_int('top_k')
id_index = attrs.get_int('id_index')
invalid_to_bottom = attrs.get_bool('invalid_to_bottom')

return topi.vision.nms(inputs[0], inputs[1], nms_threshold,
force_suppress, nms_topk)
return topi.vision.non_max_suppression(inputs[0], inputs[1], max_output_size,
iou_threshold, force_suppress, top_k,
id_index, return_indices, invalid_to_bottom)

reg.register_pattern("nms", OpPattern.OPAQUE)
reg.register_pattern("non_max_suppression", OpPattern.OPAQUE)
21 changes: 15 additions & 6 deletions nnvm/src/top/vision/nms.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ using compiler::FTVMCompute;
using tvm::Tensor;
using tvm::Array;

DMLC_REGISTER_PARAMETER(NMSParam);
DMLC_REGISTER_PARAMETER(NonMaximumSuppressionParam);

bool NMSShape(const NodeAttrs& attrs,
std::vector<TShape> *in_attrs,
std::vector<TShape> *out_attrs) {
const NonMaximumSuppressionParam& param =
nnvm::get<NonMaximumSuppressionParam>(attrs.parsed);
CHECK_EQ(in_attrs->size(), 2U) << "Inputs: [data, valid_count]";
TShape dshape = in_attrs->at(0);
TShape vshape = in_attrs->at(1);
Expand All @@ -33,7 +35,14 @@ bool NMSShape(const NodeAttrs& attrs,
"(batch_size, num_anchors, 6).";
CHECK_EQ(dshape[0], vshape[0]) << "batch_size mismatch.";
out_attrs->clear();
NNVM_ASSIGN_OUTPUT_SHAPE(attrs, *out_attrs, 0, dshape);
if (param.return_indices) {
TShape oshape = TShape(2);
oshape[0] = dshape[0];
oshape[1] = dshape[1];
NNVM_ASSIGN_OUTPUT_SHAPE(attrs, *out_attrs, 0, oshape);
} else {
NNVM_ASSIGN_OUTPUT_SHAPE(attrs, *out_attrs, 0, dshape);
}
return true;
}

Expand All @@ -56,15 +65,15 @@ inline bool NMSInferLayout(const NodeAttrs& attrs,
return true;
}

NNVM_REGISTER_OP(nms)
NNVM_REGISTER_OP(non_max_suppression)
.describe(R"doc("Non-maximum suppression."
)doc" NNVM_ADD_FILELINE)
.set_num_inputs(2)
.set_num_outputs(1)
.set_attr_parser(ParamParser<NMSParam>)
.set_attr_parser(ParamParser<NonMaximumSuppressionParam>)
.set_attr<FGetAttrDict>("FGetAttrDict",
ParamGetAttrDict<NMSParam>)
.add_arguments(NMSParam::__FIELDS__())
ParamGetAttrDict<NonMaximumSuppressionParam>)
.add_arguments(NonMaximumSuppressionParam::__FIELDS__())
.add_argument("data", "Tensor", "Input data.")
.add_argument("valid_count", "Tensor", "Number of valid anchor boxes.")
.set_attr<FListInputNames>("FListInputNames", [](const NodeAttrs& attrs) {
Expand Down
16 changes: 8 additions & 8 deletions nnvm/tests/python/compiler/test_top_level4.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ def test_multibox_transform_loc():
anchors = sym.Variable("anchors")
transform_loc_data, valid_count = sym.multibox_transform_loc(cls_prob=cls_prob, loc_pred=loc_preds,
anchor=anchors)
out = sym.nms(data=transform_loc_data, valid_count=valid_count)
out = sym.non_max_suppression(data=transform_loc_data, valid_count=valid_count, return_indices=False)

# Manually create test case
np_cls_prob = np.array([[[0.2, 0.5, 0.3], [0.25, 0.3, 0.45], [0.7, 0.1, 0.2]]])
Expand All @@ -573,22 +573,22 @@ def test_multibox_transform_loc():
out = m.get_output(0, tvm.nd.empty(expected_np_out.shape, dtype))
tvm.testing.assert_allclose(out.asnumpy(), expected_np_out, atol=1e-5, rtol=1e-5)

def test_nms():
def test_non_max_suppression():
dshape = (1, 5, 6)
data = sym.Variable("data")
valid_count = sym.Variable("valid_count", dtype="int32")
nms_threshold = 0.7
iou_threshold = 0.7
force_suppress = True
nms_topk = 2
out = sym.nms(data=data, valid_count=valid_count, nms_threshold=nms_threshold,
force_suppress=force_suppress, nms_topk=nms_topk)
top_k = 2
out = sym.non_max_suppression(data=data, valid_count=valid_count, return_indices=False,
iou_threshold=iou_threshold, force_suppress=force_suppress, top_k=top_k)

np_data = np.array([[[0, 0.8, 1, 20, 25, 45], [1, 0.7, 30, 60, 50, 80],
[0, 0.4, 4, 21, 19, 40], [2, 0.9, 35, 61, 52, 79],
[1, 0.5, 100, 60, 70, 110]]]).astype("float32")
np_valid_count = np.array([4]).astype("int32")
np_result = np.array([[[2, 0.9, 35, 61, 52, 79], [0, 0.8, 1, 20, 25, 45],
[0, 0.4, 4, 21, 19, 40], [-1, 0.9, 35, 61, 52, 79],
[-1, -1, -1, -1, -1, -1], [-1, -1, -1, -1, -1, -1],
[-1, -1, -1, -1, -1, -1]]])

target = "llvm"
Expand Down Expand Up @@ -726,7 +726,7 @@ def test_argmax():
test_flip()
test_multibox_prior()
test_multibox_transform_loc()
test_nms()
test_non_max_suppression()
test_slice_like()
test_where()
test_argmax()
Expand Down
1 change: 0 additions & 1 deletion nnvm/tests/python/frontend/mxnet/test_forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,4 +315,3 @@ def test_forward_minimum():
test_forward_slice()
test_forward_maximum()
test_forward_minimum()

54 changes: 51 additions & 3 deletions python/tvm/relay/frontend/mxnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,14 @@ def _mx_multibox_detection(inputs, attrs):
0.2, 0.2))

new_attrs1 = {}
new_attrs1["overlap_threshold"] = attrs.get_float("nms_threshold", 0.5)
new_attrs1["return_indices"] = False
new_attrs1["iou_threshold"] = attrs.get_float("nms_threshold", 0.5)
new_attrs1["force_suppress"] = attrs.get_bool("force_suppress", False)
new_attrs1["topk"] = attrs.get_int("nms_topk", -1)
new_attrs1["top_k"] = attrs.get_int("nms_topk", -1)

ret = _op.vision.multibox_transform_loc(inputs[0], inputs[1],
inputs[2], **new_attrs0)
return _op.vision.nms(ret[0], ret[1], **new_attrs1)
return _op.vision.non_max_suppression(ret[0], ret[1], **new_attrs1)


def _mx_batch_dot(inputs, attrs):
Expand Down Expand Up @@ -380,6 +381,49 @@ def _mx_proposal(inputs, attrs):
return _op.vision.proposal(inputs[0], inputs[1], inputs[2], **new_attrs)


def _mx_box_nms(inputs, attrs):
force_suppress = attrs.get_bool("force_suppress", False)
iou_thresh = attrs.get_float('overlap_thresh', 0.5)
top_k = attrs.get_int('topk', -1)
valid_thresh = attrs.get_float('valid_thresh', 0)
coord_start = attrs.get_int('coord_start', 2)
score_index = attrs.get_int('score_index', 1)
id_index = attrs.get_int('id_index', -1)
in_format = attrs.get_str('in_format', 'corner')
out_format = attrs.get_str('out_format', 'corner')
if coord_start != 2:
raise RuntimeError('coord_start %s is not supported.' % coord_start)
if score_index != 1:
raise RuntimeError('score_index %s is not supported.' % score_index)
if id_index != -1 and int(id_index) != 0:
raise RuntimeError('id_index %s is not supported.' % id_index)
if in_format != 'corner':
raise RuntimeError('in_format %s is not supported.' % in_format)
if out_format != 'corner':
raise RuntimeError('out_format %s is not supported.' % out_format)

ret = _op.vision.get_valid_counts(inputs[0], score_threshold=valid_thresh)
nms_out = _op.vision.non_max_suppression(ret[1],
ret[0],
iou_threshold=iou_thresh,
force_suppress=force_suppress,
top_k=top_k,
id_index=id_index,
return_indices=False,
invalid_to_bottom=True)
return nms_out


def _mx_l2_normalize(inputs, attrs):
new_attrs = {}
mode = attrs.get_str('mode', 'instance')
if mode != 'channel':
raise RuntimeError('mode %s is not supported.' % mode)
new_attrs['eps'] = attrs.get_float('eps', 1e-10)
new_attrs['axis'] = [1]
return _op.nn.l2_normalize(inputs[0], **new_attrs)


# Note: due to attribute conversion constraint
# ops in the identity set must be attribute free
_identity_list = [
Expand Down Expand Up @@ -478,6 +522,7 @@ def _mx_proposal(inputs, attrs):
"BatchNorm" : _mx_batch_norm,
"BatchNorm_v1" : _mx_batch_norm,
"LRN" : _mx_lrn,
"L2Normalization" : _mx_l2_normalize,
"slice" : _mx_slice,
"slice_like" : _mx_slice_like,
"slice_axis" : _mx_slice_axis,
Expand All @@ -498,6 +543,7 @@ def _mx_proposal(inputs, attrs):
"_contrib_ROIAlign" : _mx_roi_align,
"_contrib_Proposal" : _mx_proposal,
"_contrib_MultiProposal" : _mx_proposal,
"_contrib_box_nms" : _mx_box_nms,
# List of missing operators that are present in NNVMv1
# TODO(tvm-tvm): support all operators.
#
Expand Down Expand Up @@ -640,6 +686,8 @@ def from_mxnet(symbol,
params[k] = _nd.array(v.data().asnumpy())
data = mx.sym.Variable("data")
sym = symbol(data)
if isinstance(sym, (list, tuple)):
sym = mx.sym.Group(sym)
shape, dtype = _update_shape_dtype(shape, dtype, params)
sym = _from_mxnet_impl(sym, shape, dtype)
elif isinstance(symbol, mx.gluon.Block):
Expand Down
2 changes: 1 addition & 1 deletion python/tvm/relay/op/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ def strided_slice(data, begin, end, strides=None):
The indices to begin with in the slicing.

end: list of int
Indicies indicating end of the slice.
Indices indicating end of the slice.

strides: list of int, optional
Specifies the stride values, it can be negative in that case,
Expand Down
2 changes: 1 addition & 1 deletion python/tvm/relay/op/vision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
from .nms import *
from .rcnn import *
from .yolo import *
from . import _multibox
from . import _rcnn
from . import _yolo
from . import _vision
Loading