diff --git a/tfjs-backend-wasm/src/cc/BUILD b/tfjs-backend-wasm/src/cc/BUILD index aa3d7f61e5e..d41cca267f1 100644 --- a/tfjs-backend-wasm/src/cc/BUILD +++ b/tfjs-backend-wasm/src/cc/BUILD @@ -104,6 +104,15 @@ tfjs_cc_library( ], ) +tfjs_cc_library( + name = "non_max_suppression_impl", + srcs = ["non_max_suppression_impl.cc"], + hdrs = ["non_max_suppression_impl.h"], + deps = [ + ":backend", + ], +) + tfjs_cc_library( name = "prelu_impl", srcs = ["prelu_impl.cc"], @@ -169,6 +178,7 @@ tfjs_cc_library( ":Minimum", ":Mul", ":NonMaxSuppressionV3", + ":NonMaxSuppressionV5", ":PadV2", ":Prelu", ":Relu", @@ -537,6 +547,17 @@ tfjs_cc_library( srcs = ["kernels/NonMaxSuppressionV3.cc"], deps = [ ":backend", + ":non_max_suppression_impl", + ":util", + ], +) + +tfjs_cc_library( + name = "NonMaxSuppressionV5", + srcs = ["kernels/NonMaxSuppressionV5.cc"], + deps = [ + ":backend", + ":non_max_suppression_impl", ":util", ], ) diff --git a/tfjs-backend-wasm/src/cc/kernels/NonMaxSuppressionV3.cc b/tfjs-backend-wasm/src/cc/kernels/NonMaxSuppressionV3.cc index 32de91300a7..1b590cb99fd 100644 --- a/tfjs-backend-wasm/src/cc/kernels/NonMaxSuppressionV3.cc +++ b/tfjs-backend-wasm/src/cc/kernels/NonMaxSuppressionV3.cc @@ -22,52 +22,7 @@ #include #include -#include "src/cc/backend.h" -#include "src/cc/util.h" - -namespace { - -float compute_iou(const float* boxes, const size_t i, const size_t j) { - const float* i_coord = boxes + i * 4; - const float* j_coord = boxes + j * 4; - - const float y_min_i = std::min(i_coord[0], i_coord[2]); - const float x_min_i = std::min(i_coord[1], i_coord[3]); - - const float y_max_i = std::max(i_coord[0], i_coord[2]); - const float x_max_i = std::max(i_coord[1], i_coord[3]); - - const float y_min_j = std::min(j_coord[0], j_coord[2]); - const float x_min_j = std::min(j_coord[1], j_coord[3]); - - const float y_max_j = std::max(j_coord[0], j_coord[2]); - const float x_max_j = std::max(j_coord[1], j_coord[3]); - - const float area_i = (y_max_i - y_min_i) * (x_max_i - x_min_i); - const float area_j = (y_max_j - y_min_j) * (x_max_j - x_min_j); - - if (area_i <= 0 || area_j <= 0) { - return 0.0; - } - - const float intersect_y_min = std::max(y_min_i, y_min_j); - const float intersect_x_min = std::max(x_min_i, x_min_j); - const float intersect_y_max = std::min(y_max_i, y_max_j); - const float intersect_x_max = std::min(x_max_i, x_max_j); - const float intersect_area = - std::max(intersect_y_max - intersect_y_min, .0f) * - std::max(intersect_x_max - intersect_x_min, .0f); - return intersect_area / (area_i + area_j - intersect_area); -} - -// Structure to store the result of the kernel. In this case we give js a -// a pointer in memory where the result is stored and how big it is. -struct Result { - int32_t* buf; - size_t size; -}; - -} // namespace +#include "src/cc/non_max_suppression_impl.h" namespace tfjs { namespace wasm { @@ -77,62 +32,14 @@ extern "C" { #ifdef __EMSCRIPTEN__ EMSCRIPTEN_KEEPALIVE #endif -const Result* NonMaxSuppressionV3(const size_t boxes_id, const size_t scores_id, - const size_t max_out_size, - const float iou_threshold, - const float score_threshold) { - auto& boxes_info = backend::get_tensor_info(boxes_id); - auto& scores_info = backend::get_tensor_info_out(scores_id); - const float* boxes = boxes_info.f32(); - const float* scores = scores_info.f32(); - const size_t num_boxes = boxes_info.size / 4; - - // Filter out boxes that are below the score threshold. - std::vector box_indices; - for (int32_t i = 0; i < num_boxes; ++i) { - if (scores[i] > score_threshold) { - box_indices.push_back(i); - } - } - - // Sort by remaining boxes by scores. - std::sort(box_indices.begin(), box_indices.end(), - [&scores](const size_t i, const size_t j) { - return scores[i] > scores[j]; - }); - - // Select a box only if it doesn't overlap beyond the threshold with the - // already selected boxes. - std::vector selected; - for (int32_t i = 0; i < box_indices.size(); ++i) { - const size_t box_i = box_indices[i]; - bool ignore_candidate = false; - for (int32_t j = 0; j < selected.size(); ++j) { - const int32_t box_j = selected[j]; - const float iou = compute_iou(boxes, box_i, box_j); - if (iou >= iou_threshold) { - ignore_candidate = true; - break; - } - } - if (!ignore_candidate) { - selected.push_back(box_i); - if (selected.size() >= max_out_size) { - break; - } - } - } - - // Allocate memory on the heap for the resulting indices and copy the data - // from the `selected` vector since we can't "steal" the data from the - // vector. - int32_t* data = - static_cast(malloc(selected.size() * sizeof(int32_t))); - std::memcpy(data, selected.data(), selected.size() * sizeof(int32_t)); - - // Allocate the result of the method on the heap so it survives past this - // function and we can read it in js. - return new Result{data, selected.size()}; +const NonMaxSuppressionResult* NonMaxSuppressionV3( + const size_t boxes_id, const size_t scores_id, const size_t max_out_size, + const float iou_threshold, const float score_threshold) { + const float dummy_soft_nms_sigma = 0.0; + + return tfjs::wasm::non_max_suppression_impl(boxes_id, scores_id, max_out_size, + iou_threshold, score_threshold, + dummy_soft_nms_sigma); } } // extern "C" diff --git a/tfjs-backend-wasm/src/cc/kernels/NonMaxSuppressionV5.cc b/tfjs-backend-wasm/src/cc/kernels/NonMaxSuppressionV5.cc new file mode 100644 index 00000000000..3cee240ed2f --- /dev/null +++ b/tfjs-backend-wasm/src/cc/kernels/NonMaxSuppressionV5.cc @@ -0,0 +1,50 @@ +/* Copyright 2019 Google Inc. 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. + * ===========================================================================*/ + +#ifdef __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "src/cc/non_max_suppression_impl.h" + +namespace tfjs { +namespace wasm { +// We use C-style API to interface with Javascript. +extern "C" { + +#ifdef __EMSCRIPTEN__ +EMSCRIPTEN_KEEPALIVE +#endif +const NonMaxSuppressionResult* NonMaxSuppressionV5(const size_t boxes_id, + const size_t scores_id, + const size_t max_out_size, + const float iou_threshold, + const float score_threshold, + const float soft_nms_sigma) { + return tfjs::wasm::non_max_suppression_impl(boxes_id, scores_id, max_out_size, + iou_threshold, score_threshold, + soft_nms_sigma); +} + +} // extern "C" +} // namespace wasm +} // namespace tfjs diff --git a/tfjs-backend-wasm/src/cc/non_max_suppression_impl.cc b/tfjs-backend-wasm/src/cc/non_max_suppression_impl.cc new file mode 100644 index 00000000000..f24707896da --- /dev/null +++ b/tfjs-backend-wasm/src/cc/non_max_suppression_impl.cc @@ -0,0 +1,200 @@ +/* Copyright 2019 Google Inc. 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. + * ===========================================================================*/ + +#ifdef __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "src/cc/backend.h" +#include "src/cc/non_max_suppression_impl.h" + +namespace { + +struct Candidate { + int32_t box_index; + float score; + int32_t suppress_begin_index; +}; + +float compute_iou(const float* boxes, const size_t i, const size_t j) { + const float* i_coord = boxes + i * 4; + const float* j_coord = boxes + j * 4; + + const float y_min_i = std::min(i_coord[0], i_coord[2]); + const float x_min_i = std::min(i_coord[1], i_coord[3]); + + const float y_max_i = std::max(i_coord[0], i_coord[2]); + const float x_max_i = std::max(i_coord[1], i_coord[3]); + + const float y_min_j = std::min(j_coord[0], j_coord[2]); + const float x_min_j = std::min(j_coord[1], j_coord[3]); + + const float y_max_j = std::max(j_coord[0], j_coord[2]); + const float x_max_j = std::max(j_coord[1], j_coord[3]); + + const float area_i = (y_max_i - y_min_i) * (x_max_i - x_min_i); + const float area_j = (y_max_j - y_min_j) * (x_max_j - x_min_j); + + if (area_i <= 0 || area_j <= 0) { + return 0.0; + } + + const float intersect_y_min = std::max(y_min_i, y_min_j); + const float intersect_x_min = std::max(x_min_i, x_min_j); + const float intersect_y_max = std::min(y_max_i, y_max_j); + const float intersect_x_max = std::min(x_max_i, x_max_j); + const float intersect_area = + std::max(intersect_y_max - intersect_y_min, .0f) * + std::max(intersect_x_max - intersect_x_min, .0f); + return intersect_area / (area_i + area_j - intersect_area); +} + +float suppress_weight(const float iou_threshold, const float scale, + const float iou) { + const float weight = std::exp(scale * iou * iou); + return iou <= iou_threshold ? weight : 0.0; +} +} // namespace + +namespace tfjs { +namespace wasm { +const NonMaxSuppressionResult* non_max_suppression_impl( + const size_t boxes_id, const size_t scores_id, const size_t max_out_size, + const float iou_threshold, const float score_threshold, + const float soft_nms_sigma) { + auto& boxes_info = backend::get_tensor_info(boxes_id); + auto& scores_info = backend::get_tensor_info_out(scores_id); + const float* boxes = boxes_info.f32(); + const float* scores = scores_info.f32(); + const size_t num_boxes = boxes_info.size / 4; + + auto score_comparator = [](const Candidate i, const Candidate j) { + return i.score < j.score || + ((i.score == j.score) && (i.box_index > j.box_index)); + }; + // Construct a max heap by candidate scores. + std::priority_queue, + decltype(score_comparator)> + candidate_priority_queue(score_comparator); + + const int32_t suppress_at_start = 0; + // Filter out boxes that are below the score threshold and also maintain + // the order of boxes by scores. + for (int32_t i = 0; i < num_boxes; i++) { + if (scores[i] > score_threshold) { + candidate_priority_queue.emplace( + Candidate({i, scores[i], suppress_at_start})); + } + } + + // If soft_nms_sigma is 0, the outcome of this algorithm is exactly same as + // before. + const float scale = soft_nms_sigma > 0.0 ? (-0.5 / soft_nms_sigma) : 0.0; + + // Select a box only if it doesn't overlap beyond the threshold with the + // already selected boxes. + std::vector selected_indices; + std::vector selected_scores; + Candidate candidate; + float iou, original_score; + + while (selected_indices.size() < max_out_size && + !candidate_priority_queue.empty()) { + candidate = candidate_priority_queue.top(); + original_score = candidate.score; + candidate_priority_queue.pop(); + + if (original_score < score_threshold) { + break; + } + + // Overlapping boxes are likely to have similar scores, therefore we + // iterate through the previously selected boxes backwards in order to + // see if candidate's score should be suppressed. We use + // suppress_begin_index to track and ensure a candidate can be suppressed + // by a selected box no more than once. Also, if the overlap exceeds + // iou_threshold, we simply ignore the candidate. + bool ignore_candidate = false; + for (int32_t j = selected_indices.size() - 1; + j >= candidate.suppress_begin_index; --j) { + const float iou = + compute_iou(boxes, candidate.box_index, selected_indices[j]); + + if (iou >= iou_threshold) { + ignore_candidate = true; + break; + } + + candidate.score *= suppress_weight(iou_threshold, scale, iou); + + if (candidate.score <= score_threshold) { + break; + } + } + + // At this point, if `candidate.score` has not dropped below + // `score_threshold`, then we know that we went through all of the + // previous selections and can safely update `suppress_begin_index` to the + // end of the selected array. Then we can re-insert the candidate with + // the updated score and suppress_begin_index back in the candidate queue. + // If on the other hand, `candidate.score` has dropped below the score + // threshold, we will not add it back to the candidates queue. + candidate.suppress_begin_index = selected_indices.size(); + + if (!ignore_candidate) { + // Candidate has passed all the tests, and is not suppressed, so + // select the candidate. + if (candidate.score == original_score) { + selected_indices.push_back(candidate.box_index); + selected_scores.push_back(candidate.score); + } else if (candidate.score > score_threshold) { + // Candidate's score is suppressed but is still high enough to be + // considered, so add back to the candidates queue. + candidate_priority_queue.push(candidate); + } + } + } + + // Allocate memory on the heap for the results and copy the data from the + // `selected_indices` and `selected_scores` vector since we can't "steal" the + // data from the vector. + size_t selected_indices_data_size = selected_indices.size() * sizeof(int32_t); + int32_t* selected_indices_data = + static_cast(malloc(selected_indices_data_size)); + std::memcpy(selected_indices_data, selected_indices.data(), + selected_indices_data_size); + + size_t selected_scores_data_size = selected_scores.size() * sizeof(float); + float* selected_scores_data = + static_cast(malloc(selected_scores_data_size)); + std::memcpy(selected_scores_data, selected_scores.data(), + selected_scores_data_size); + + // Allocate the result of the method on the heap so it survives past this + // function and we can read it in js. + return new NonMaxSuppressionResult{ + selected_indices_data, selected_indices.size(), selected_scores_data}; +} + +} // namespace wasm +} // namespace tfjs diff --git a/tfjs-backend-wasm/src/cc/non_max_suppression_impl.h b/tfjs-backend-wasm/src/cc/non_max_suppression_impl.h new file mode 100644 index 00000000000..b2fe9903d3d --- /dev/null +++ b/tfjs-backend-wasm/src/cc/non_max_suppression_impl.h @@ -0,0 +1,40 @@ +/* Copyright 2019 Google Inc. 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. + * ===========================================================================*/ + +#ifndef NON_MAX_SUPPRESSION_IMPL_H_ +#define NON_MAX_SUPPRESSION_IMPL_H_ + +#include +#include + +namespace tfjs { +namespace wasm { + +// Structure to store the result of the kernel. In this case we give js a +// a pointer in memory where the result is stored and how big it is. +struct NonMaxSuppressionResult { + int32_t* selected_indices; + size_t selected_size; + float* selected_scores; +}; + +const NonMaxSuppressionResult* non_max_suppression_impl( + const size_t boxes_id, const size_t scores_id, const size_t max_out_size, + const float iou_threshold, const float score_threshold, + const float soft_nms_sigma); + +} // namespace wasm +} // namespace tfjs + +#endif // NON_MAX_SUPPRESSION_IMPL_H_ diff --git a/tfjs-backend-wasm/src/kernels/NonMaxSuppressionV3.ts b/tfjs-backend-wasm/src/kernels/NonMaxSuppressionV3.ts index 91332f3bd7d..afaa6d31c3d 100644 --- a/tfjs-backend-wasm/src/kernels/NonMaxSuppressionV3.ts +++ b/tfjs-backend-wasm/src/kernels/NonMaxSuppressionV3.ts @@ -19,6 +19,8 @@ import {NamedAttrMap, NamedTensorInfoMap, registerKernel, TensorInfo} from '@ten import {BackendWasm} from '../backend_wasm'; +import {parseResultStruct} from './NonMaxSuppression_util'; + interface NonMaxSuppressionInputs extends NamedTensorInfoMap { boxes: TensorInfo; scores: TensorInfo; @@ -30,27 +32,6 @@ interface NonMaxSuppressionAttrs extends NamedAttrMap { scoreThreshold: number; } -// Analogous to `struct Result` in `NonMaxSuppressionV3.cc`. -interface Result { - memOffset: number; - size: number; -} - -/** - * Parse the result of the c++ method, which is a data structure with two ints - * (memOffset and size). - */ -function parseResultStruct(backend: BackendWasm, resOffset: number): Result { - // The result of c++ method is a data structure with two ints (memOffset, and - // size). - const result = new Int32Array(backend.wasm.HEAPU8.buffer, resOffset, 2); - const memOffset = result[0]; - const size = result[1]; - // Since the result was allocated on the heap, we have to delete it. - backend.wasm._free(resOffset); - return {memOffset, size}; -} - let wasmFunc: ( boxesId: number, scoresId: number, maxOutputSize: number, iouThreshold: number, scoreThreshold: number) => number; @@ -83,10 +64,16 @@ function kernelFunc(args: { const resOffset = wasmFunc(boxesId, scoresId, maxOutputSize, iouThreshold, scoreThreshold); - const {memOffset, size} = parseResultStruct(backend, resOffset); + const {pSelectedIndices, selectedSize, pSelectedScores} = + parseResultStruct(backend, resOffset); + + // Since we are not using scores for V3, we have to delete it from the heap. + backend.wasm._free(pSelectedScores); + + const selectedIndicesTensor = + backend.makeOutput([selectedSize], 'int32', pSelectedIndices); - const outShape = [size]; - return backend.makeOutput(outShape, 'int32', memOffset); + return selectedIndicesTensor; } registerKernel({ diff --git a/tfjs-backend-wasm/src/kernels/NonMaxSuppressionV5.ts b/tfjs-backend-wasm/src/kernels/NonMaxSuppressionV5.ts new file mode 100644 index 00000000000..ded17d2a93a --- /dev/null +++ b/tfjs-backend-wasm/src/kernels/NonMaxSuppressionV5.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright 2019 Google Inc. 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 {NamedAttrMap, NamedTensorInfoMap, registerKernel, TensorInfo} from '@tensorflow/tfjs-core'; + +import {BackendWasm} from '../backend_wasm'; + +import {parseResultStruct} from './NonMaxSuppression_util'; + +interface NonMaxSuppressionInputs extends NamedTensorInfoMap { + boxes: TensorInfo; + scores: TensorInfo; +} + +interface NonMaxSuppressionAttrs extends NamedAttrMap { + maxOutputSize: number; + iouThreshold: number; + scoreThreshold: number; + softNmsSigma: number; +} + +let wasmFunc: + (boxesId: number, scoresId: number, maxOutputSize: number, + iouThreshold: number, scoreThreshold: number, softNmsSigma: number) => + number; + +function setup(backend: BackendWasm): void { + wasmFunc = backend.wasm.cwrap( + 'NonMaxSuppressionV5', + 'number', // Result* + [ + 'number', // boxesId + 'number', // scoresId + 'number', // maxOutputSize + 'number', // iouThreshold + 'number', // scoreThreshold + 'number', // softNmsSigma + ]); +} + +function kernelFunc(args: { + backend: BackendWasm, + inputs: NonMaxSuppressionInputs, + attrs: NonMaxSuppressionAttrs +}): TensorInfo[] { + const {backend, inputs, attrs} = args; + const {iouThreshold, maxOutputSize, scoreThreshold, softNmsSigma} = attrs; + const {boxes, scores} = inputs; + + const boxesId = backend.dataIdMap.get(boxes.dataId).id; + const scoresId = backend.dataIdMap.get(scores.dataId).id; + + const resOffset = wasmFunc( + boxesId, scoresId, maxOutputSize, iouThreshold, scoreThreshold, + softNmsSigma); + + const { + pSelectedIndices, + selectedSize, + pSelectedScores, + } = parseResultStruct(backend, resOffset); + + const selectedIndicesTensor = + backend.makeOutput([selectedSize], 'int32', pSelectedIndices); + const selectedScoresTensor = + backend.makeOutput([selectedSize], 'float32', pSelectedScores); + + return [selectedIndicesTensor, selectedScoresTensor]; +} + +registerKernel({ + kernelName: 'NonMaxSuppressionV5', + backendName: 'wasm', + setupFunc: setup, + kernelFunc, +}); diff --git a/tfjs-backend-wasm/src/kernels/NonMaxSuppression_util.ts b/tfjs-backend-wasm/src/kernels/NonMaxSuppression_util.ts new file mode 100644 index 00000000000..b976e0f8058 --- /dev/null +++ b/tfjs-backend-wasm/src/kernels/NonMaxSuppression_util.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2019 Google Inc. 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 {BackendWasm} from '../backend_wasm'; + +// Analogous to `struct Result` in `non_max_suppression_impl.h`. +interface Result { + pSelectedIndices: number; + selectedSize: number; + pSelectedScores: number; +} +/** + * Parse the result of the c++ method, which has the shape equivalent to + * `Result`. + */ +export function parseResultStruct( + backend: BackendWasm, resOffset: number): Result { + const result = new Int32Array(backend.wasm.HEAPU8.buffer, resOffset, 3); + const pSelectedIndices = result[0]; + const selectedSize = result[1]; + const pSelectedScores = result[2]; + // Since the result was allocated on the heap, we have to delete it. + backend.wasm._free(resOffset); + return {pSelectedIndices, selectedSize, pSelectedScores}; +} diff --git a/tfjs-backend-wasm/src/kernels/all_kernels.ts b/tfjs-backend-wasm/src/kernels/all_kernels.ts index 0a47983ee08..59de2028718 100644 --- a/tfjs-backend-wasm/src/kernels/all_kernels.ts +++ b/tfjs-backend-wasm/src/kernels/all_kernels.ts @@ -52,6 +52,7 @@ import './Min'; import './Minimum'; import './Mul'; import './NonMaxSuppressionV3'; +import './NonMaxSuppressionV5'; import './PadV2'; import './Prelu'; import './Relu'; diff --git a/tfjs-backend-wasm/src/setup_test.ts b/tfjs-backend-wasm/src/setup_test.ts index 005cb3a7e86..9b794fa25f0 100644 --- a/tfjs-backend-wasm/src/setup_test.ts +++ b/tfjs-backend-wasm/src/setup_test.ts @@ -211,7 +211,7 @@ const TEST_FILTERS: TestFilter[] = [ {include: 'pad ', excludes: ['complex', 'zerosLike']}, {include: 'clip', excludes: ['gradient']}, {include: 'addN'}, - {include: 'nonMaxSuppression', excludes: ['SoftNMS']}, + {include: 'nonMaxSuppression'}, {include: 'argmax', excludes: ['gradient']}, {include: 'exp '}, {include: 'unstack'},