From cd277360e2a7649692320abdaaa741968dfece7d Mon Sep 17 00:00:00 2001 From: weiwei zhou Date: Fri, 4 Dec 2020 22:32:27 +0800 Subject: [PATCH] create psenet project with weight from tensorflow (#321) * create psenet create psenet with weight from tensorflow * delete some useless code * repalce tab with 4 blanks --- psenet/CMakeLists.txt | 40 ++++ psenet/README.md | 53 +++++ psenet/gen_tf_wts.py | 31 +++ psenet/layers.cpp | 136 +++++++++++++ psenet/layers.h | 18 ++ psenet/main.cpp | 36 ++++ psenet/psenet.cpp | 451 ++++++++++++++++++++++++++++++++++++++++++ psenet/psenet.h | 39 ++++ psenet/test.jpg | Bin 0 -> 43075 bytes psenet/utils.cpp | 73 +++++++ psenet/utils.h | 82 ++++++++ 11 files changed, 959 insertions(+) create mode 100644 psenet/CMakeLists.txt create mode 100644 psenet/README.md create mode 100644 psenet/gen_tf_wts.py create mode 100644 psenet/layers.cpp create mode 100644 psenet/layers.h create mode 100644 psenet/main.cpp create mode 100644 psenet/psenet.cpp create mode 100644 psenet/psenet.h create mode 100644 psenet/test.jpg create mode 100644 psenet/utils.cpp create mode 100644 psenet/utils.h diff --git a/psenet/CMakeLists.txt b/psenet/CMakeLists.txt new file mode 100644 index 0000000..b9c6ef2 --- /dev/null +++ b/psenet/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 2.6) + +project(PSENet) + +add_definitions(-std=c++11) + +option(CUDA_USE_STATIC_CUDA_RUNTIME OFF) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_BUILD_TYPE Debug) + +find_package(CUDA REQUIRED) + +set(CUDA_NVCC_PLAGS ${CUDA_NVCC_PLAGS};-std=c++11;-g;-G;-gencode;arch=compute_30;code=sm_30) + +include_directories(${PROJECT_SOURCE_DIR}/include) +# include and link dirs of cuda and tensorrt, you need adapt them if yours are different +# cuda +include_directories(/usr/local/cuda/include) +link_directories(/usr/local/cuda/lib64) +# tensorrt +include_directories(/usr/include/x86_64-linux-gnu/) +link_directories(/usr/lib/x86_64-linux-gnu/) + + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Ofast -Wfatal-errors -D_MWAITXINTRIN_H_INCLUDED") + + + +find_package(OpenCV) +include_directories(OpenCV_INCLUDE_DIRS) + +file(GLOB SOURCE_FILES "*.h" "*.cpp") + +add_executable(psenet ${SOURCE_FILES}) +target_link_libraries(psenet nvinfer) +target_link_libraries(psenet cudart) +target_link_libraries(psenet ${OpenCV_LIBS}) + +add_definitions(-O2 -pthread) + diff --git a/psenet/README.md b/psenet/README.md new file mode 100644 index 0000000..e019609 --- /dev/null +++ b/psenet/README.md @@ -0,0 +1,53 @@ +# PSENet + +**preprocessing + inference + postprocessing = 30ms** with fp32 on Tesla P40. +The Tensorflow implementation is [tensorflow_PSENet](https://github.com/liuheng92/tensorflow_PSENet). + +## Key Features +- Generating `.wts` from `Tensorflow`. +- Dynamic batch and dynamic shape input. +- Object-Oriented Programming. +- Practice with C++ 11. + + +

+ +## How to Run + +* 1. generate .wts + + Download pretrained model from https://github.com/liuheng92/tensorflow_PSENet + and put `model.ckpt.*` to `model` dir. Add a file `model/checkpoint` with content + ``` + model_checkpoint_path: "model.ckpt" + all_model_checkpoint_paths: "model.ckpt" + ``` + Then run + + ``` + python gen_tf_wts.py + ``` + which will gengerate a `psenet.wts`. +* 2. cmake and make + + ``` + mkdir build + cd build + cmake .. + make + ``` +* 3. build engine and run detection + ``` + cp ../psenet.wts ./ + cp ../test.jpg ./ + ./psenet -s // serialize model to plan file + ./psenet -d // deserialize plan file and run inference" + ``` + +## Known Issues +1. The output of network is not completely the same as the tf's due to the difference between tensorrt's `addResize` and `tf.image.resize`, I will figure it out. + +## Todo + +* use `ExponentialMovingAverage` weight. +* faster preporcess and postprocess. \ No newline at end of file diff --git a/psenet/gen_tf_wts.py b/psenet/gen_tf_wts.py new file mode 100644 index 0000000..0501b75 --- /dev/null +++ b/psenet/gen_tf_wts.py @@ -0,0 +1,31 @@ +from sys import prefix +import tensorflow as tf +from tensorflow.python import pywrap_tensorflow +import numpy as np +import struct + +model_dir = "model" + +ckpt = tf.train.get_checkpoint_state(model_dir) +ckpt_path = ckpt.model_checkpoint_path + +reader = pywrap_tensorflow.NewCheckpointReader(ckpt_path) +param_dict = reader.get_variable_to_shape_map() + + +f = open(r"psenet.wts", "w") +keys = param_dict.keys() +f.write("{}\n".format(len(keys))) + +for key in keys: + weight = reader.get_tensor(key) + print(key, weight.shape) + if len(weight.shape) == 4: + weight = np.transpose(weight, (3, 2, 0, 1)) + print(weight.shape) + weight = np.reshape(weight, -1) + f.write("{} {} ".format(key, len(weight))) + for w in weight: + f.write(" ") + f.write(struct.pack(">f", float(w)).hex()) + f.write("\n") \ No newline at end of file diff --git a/psenet/layers.cpp b/psenet/layers.cpp new file mode 100644 index 0000000..acbaba0 --- /dev/null +++ b/psenet/layers.cpp @@ -0,0 +1,136 @@ +#include "layers.h" + +IScaleLayer *addBatchNorm2d(INetworkDefinition *network, std::map &weightMap, ITensor &input, std::string lname, float eps) +{ + float *gamma = (float *)weightMap[lname + "gamma"].values; // scale + float *beta = (float *)weightMap[lname + "beta"].values; // offset + float *mean = (float *)weightMap[lname + "moving_mean"].values; + float *var = (float *)weightMap[lname + "moving_variance"].values; + int len = weightMap[lname + "moving_variance"].count; + + float *scval = reinterpret_cast(malloc(sizeof(float) * len)); + for (auto i = 0; i < len; i++) + { + scval[i] = gamma[i] / sqrt(var[i] + eps); + } + Weights scale{DataType::kFLOAT, scval, len}; + + float *shval = reinterpret_cast(malloc(sizeof(float) * len)); + for (auto i = 0; i < len; i++) + { + shval[i] = beta[i] - mean[i] * gamma[i] / sqrt(var[i] + eps); + } + Weights shift{DataType::kFLOAT, shval, len}; + + float *pval = reinterpret_cast(malloc(sizeof(float) * len)); + for (auto i = 0; i < len; i++) + { + pval[i] = 1.0; + } + Weights power{DataType::kFLOAT, pval, len}; + + IScaleLayer *scale_1 = network->addScale(input, ScaleMode::kCHANNEL, shift, scale, power); + assert(scale_1); + return scale_1; +} + +IActivationLayer *bottleneck(INetworkDefinition *network, std::map &weightMap, ITensor &input, int ch, int stride, std::string lname, int branch_type) +{ + + Weights emptywts{DataType::kFLOAT, nullptr, 0}; + + IConvolutionLayer *conv1 = network->addConvolution(input, ch, DimsHW{1, 1}, weightMap[lname + "conv1/weights"], emptywts); + assert(conv1); + + Dims conv1_shape = conv1->getOutput(0)->getDimensions(); + + IScaleLayer *bn1 = addBatchNorm2d(network, weightMap, *conv1->getOutput(0), lname + "conv1/BatchNorm/", 1e-5); + assert(bn1); + + Dims bn1_shape = bn1->getOutput(0)->getDimensions(); + + IActivationLayer *relu1 = network->addActivation(*bn1->getOutput(0), ActivationType::kRELU); + assert(relu1); + + Dims relu1_shape = relu1->getOutput(0)->getDimensions(); + + IConvolutionLayer *conv2 = network->addConvolution(*relu1->getOutput(0), ch, DimsHW{3, 3}, weightMap[lname + "conv2/weights"], emptywts); + assert(conv2); + conv2->setStride(DimsHW{stride, stride}); + conv2->setPadding(DimsHW{1, 1}); + + Dims conv2_shape = conv2->getOutput(0)->getDimensions(); + + IScaleLayer *bn2 = addBatchNorm2d(network, weightMap, *conv2->getOutput(0), lname + "conv2/BatchNorm/", 1e-5); + assert(bn2); + + Dims bn2_shape = bn2->getOutput(0)->getDimensions(); + + IActivationLayer *relu2 = network->addActivation(*bn2->getOutput(0), ActivationType::kRELU); + assert(relu2); + + Dims relu2_shape = relu2->getOutput(0)->getDimensions(); + + IConvolutionLayer *conv3 = network->addConvolution(*relu2->getOutput(0), ch * 4, DimsHW{1, 1}, weightMap[lname + "conv3/weights"], emptywts); + assert(conv3); + + Dims conv3_shape = conv3->getOutput(0)->getDimensions(); + + IScaleLayer *bn3 = addBatchNorm2d(network, weightMap, *conv3->getOutput(0), lname + "conv3/BatchNorm/", 1e-5); + assert(bn3); + IElementWiseLayer *ew1; + Dims ew1_shape; + + // branch_type 0:shortcut,1:conv+bn+shortcut,2:maxpool+shortcut + if (branch_type == 0) + { + ew1 = network->addElementWise(input, *bn3->getOutput(0), ElementWiseOperation::kSUM); + assert(ew1); + ew1_shape = ew1->getOutput(0)->getDimensions(); + assert(ew1); + } + else if (branch_type == 1) + { + IConvolutionLayer *conv4 = network->addConvolution(input, ch * 4, DimsHW{1, 1}, weightMap[lname + "shortcut/weights"], emptywts); + assert(conv4); + conv4->setStride(DimsHW{stride, stride}); + IScaleLayer *bn4 = addBatchNorm2d(network, weightMap, *conv4->getOutput(0), lname + "shortcut/BatchNorm/", 1e-5); + assert(bn4); + ew1 = network->addElementWise(*bn4->getOutput(0), *bn3->getOutput(0), ElementWiseOperation::kSUM); + assert(ew1); + ew1_shape = ew1->getOutput(0)->getDimensions(); + assert(ew1); + } + else + { + IPoolingLayer *pool = network->addPoolingNd(input, PoolingType::kMAX, DimsHW{1, 1}); + assert(pool); + pool->setStrideNd(DimsHW{2, 2}); + ew1 = network->addElementWise(*pool->getOutput(0), *bn3->getOutput(0), ElementWiseOperation::kSUM); + assert(ew1); + ew1_shape = ew1->getOutput(0)->getDimensions(); + assert(ew1); + } + + IActivationLayer *relu3 = network->addActivation(*ew1->getOutput(0), ActivationType::kRELU); + + Dims relu3_shape = relu3->getOutput(0)->getDimensions(); + + assert(relu3); + return relu3; +} + +IActivationLayer *ConvRelu(INetworkDefinition *network, std::map &weightMap, ITensor &input, int outch, int kernel, int stride, std::string lname) +{ + IConvolutionLayer *conv = network->addConvolution(input, 256, DimsHW{kernel, kernel}, weightMap[lname + "weights"], weightMap[lname + "biases"]); + assert(conv); + conv->setStride(DimsHW{stride, stride}); + if (kernel == 3 || stride == 2) + { + conv->setPadding(DimsHW{1, 1}); + } + + IActivationLayer *ac = network->addActivation(*conv->getOutput(0), ActivationType::kRELU); + assert(ac); + return ac; +} \ No newline at end of file diff --git a/psenet/layers.h b/psenet/layers.h new file mode 100644 index 0000000..bd85a18 --- /dev/null +++ b/psenet/layers.h @@ -0,0 +1,18 @@ +#ifndef TENSORRTX_LAYERS_H +#define TENSORRTX_LAYERS_H + +#include +#include +#include + +#include "NvInfer.h" +#include "cuda_runtime_api.h" +using namespace nvinfer1; + +IScaleLayer *addBatchNorm2d(INetworkDefinition *network, std::map &weightMap, ITensor &input, std::string lname, float eps); + +IActivationLayer *bottleneck(INetworkDefinition *network, std::map &weightMap, ITensor &input, int ch, int stride, std::string lname, int branch_type); + +IActivationLayer *ConvRelu(INetworkDefinition *network, std::map &weightMap, ITensor &input, int outch, int kernel, int stride, std::string lname); + +#endif diff --git a/psenet/main.cpp b/psenet/main.cpp new file mode 100644 index 0000000..af83730 --- /dev/null +++ b/psenet/main.cpp @@ -0,0 +1,36 @@ +#include "psenet.h" + +int main(int argc, char **argv) +{ + PSENet psenet(1600, 0.9, 6, 4); + + if (argc == 2 && std::string(argv[1]) == "-s") + { + std::cout << "Serializling Engine" << std::endl; + psenet.serializeEngine(); + return 0; + } + else if (argc == 2 && std::string(argv[1]) == "-d") + { + psenet.init(); + std::vector files; + for (int i = 0; i < 10; i++) + { + files.emplace_back("test.jpg"); + } + for (auto file : files) + { + std::cout << "Detect " << file << std::endl; + psenet.detect(file); + } + + return 0; + } + else + { + std::cerr << "arguments not right!" << std::endl; + std::cerr << "./psenet -s // serialize model to plan file" << std::endl; + std::cerr << "./psenet -d // deserialize plan file and run inference" << std::endl; + return -1; + } +} diff --git a/psenet/psenet.cpp b/psenet/psenet.cpp new file mode 100644 index 0000000..85be8e9 --- /dev/null +++ b/psenet/psenet.cpp @@ -0,0 +1,451 @@ +#include "psenet.h" + +#define MAX_INPUT_SIZE 1200 +#define MIN_INPUT_SIZE 128 +#define OPT_INPUT_W 640 +#define OPT_INPUT_H 640 + +PSENet::PSENet(int max_side_len, float threshold, int num_kernel, int stride) : max_side_len_(max_side_len), + post_threshold_(threshold), + num_kernels_(num_kernel), + stride_(stride) +{ +} + +PSENet::~PSENet() +{ +} + +// create the engine using only the API and not any parser. +ICudaEngine *PSENet::createEngine(IBuilder *builder, IBuilderConfig *config) +{ + std::map weightMap = loadWeights("./psenet.wts"); + Weights emptywts{DataType::kFLOAT, nullptr, 0}; + const auto explicitBatch = 1U << static_cast(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); + INetworkDefinition *network = builder->createNetworkV2(explicitBatch); + + ITensor *data = network->addInput(input_name_, dt, Dims4{-1, 3, -1, -1}); + assert(data); + + IConvolutionLayer *conv1 = network->addConvolutionNd(*data, 64, DimsHW{7, 7}, weightMap["resnet_v1_50/conv1/weights"], emptywts); + conv1->setStrideNd(DimsHW{2, 2}); + conv1->setPaddingNd(DimsHW{3, 3}); + assert(conv1); + + IScaleLayer *bn1 = addBatchNorm2d(network, weightMap, *conv1->getOutput(0), "resnet_v1_50/conv1/BatchNorm/", 1e-5); + assert(bn1); + + IActivationLayer *relu1 = network->addActivation(*bn1->getOutput(0), ActivationType::kRELU); + assert(relu1); + + // C2 + IPoolingLayer *pool1 = network->addPoolingNd(*relu1->getOutput(0), PoolingType::kMAX, DimsHW{3, 3}); + pool1->setStrideNd(DimsHW{2, 2}); + pool1->setPaddingNd(DimsHW{1, 1}); + assert(pool1); + + IActivationLayer *x; + x = bottleneck(network, weightMap, *pool1->getOutput(0), 64, 1, "resnet_v1_50/block1/unit_1/bottleneck_v1/", 1); + x = bottleneck(network, weightMap, *x->getOutput(0), 64, 1, "resnet_v1_50/block1/unit_2/bottleneck_v1/", 0); + // C3 + IActivationLayer *block1 = bottleneck(network, weightMap, *x->getOutput(0), 64, 2, "resnet_v1_50/block1/unit_3/bottleneck_v1/", 2); + + x = bottleneck(network, weightMap, *block1->getOutput(0), 128, 1, "resnet_v1_50/block2/unit_1/bottleneck_v1/", 1); + x = bottleneck(network, weightMap, *x->getOutput(0), 128, 1, "resnet_v1_50/block2/unit_2/bottleneck_v1/", 0); + x = bottleneck(network, weightMap, *x->getOutput(0), 128, 1, "resnet_v1_50/block2/unit_3/bottleneck_v1/", 0); + // C4 + IActivationLayer *block2 = bottleneck(network, weightMap, *x->getOutput(0), 128, 2, "resnet_v1_50/block2/unit_4/bottleneck_v1/", 2); + + x = bottleneck(network, weightMap, *block2->getOutput(0), 256, 1, "resnet_v1_50/block3/unit_1/bottleneck_v1/", 1); + x = bottleneck(network, weightMap, *x->getOutput(0), 256, 1, "resnet_v1_50/block3/unit_2/bottleneck_v1/", 0); + x = bottleneck(network, weightMap, *x->getOutput(0), 256, 1, "resnet_v1_50/block3/unit_3/bottleneck_v1/", 0); + x = bottleneck(network, weightMap, *x->getOutput(0), 256, 1, "resnet_v1_50/block3/unit_4/bottleneck_v1/", 0); + x = bottleneck(network, weightMap, *x->getOutput(0), 256, 1, "resnet_v1_50/block3/unit_5/bottleneck_v1/", 0); + IActivationLayer *block3 = bottleneck(network, weightMap, *x->getOutput(0), 256, 2, "resnet_v1_50/block3/unit_6/bottleneck_v1/", 2); + + x = bottleneck(network, weightMap, *block3->getOutput(0), 512, 1, "resnet_v1_50/block4/unit_1/bottleneck_v1/", 1); + x = bottleneck(network, weightMap, *x->getOutput(0), 512, 1, "resnet_v1_50/block4/unit_2/bottleneck_v1/", 0); + // C5 + IActivationLayer *block4 = bottleneck(network, weightMap, *x->getOutput(0), 512, 1, "resnet_v1_50/block4/unit_3/bottleneck_v1/", 0); + + IActivationLayer *build_p5_r1 = ConvRelu(network, weightMap, *block4->getOutput(0), 256, 1, 1, "build_feature_pyramid/build_P5/"); + assert(build_p5_r1); + IActivationLayer *build_p4_r1 = ConvRelu(network, weightMap, *block2->getOutput(0), 256, 1, 1, "build_feature_pyramid/build_P4/reduce_dimension/"); + assert(build_p4_r1); + + IResizeLayer *bfp_layer4_resize = network->addResize(*build_p5_r1->getOutput(0)); + auto build_p4_r1_shape = network->addShape(*build_p4_r1->getOutput(0))->getOutput(0); + bfp_layer4_resize->setInput(1, *build_p4_r1_shape); + bfp_layer4_resize->setResizeMode(ResizeMode::kNEAREST); + bfp_layer4_resize->setAlignCorners(false); + assert(bfp_layer4_resize); + + IElementWiseLayer *bfp_add = network->addElementWise(*bfp_layer4_resize->getOutput(0), *build_p4_r1->getOutput(0), ElementWiseOperation::kSUM); + assert(bfp_add); + + IActivationLayer *build_p4_r2 = ConvRelu(network, weightMap, *bfp_add->getOutput(0), 256, 3, 1, "build_feature_pyramid/build_P4/avoid_aliasing/"); + assert(build_p4_r2); + + IActivationLayer *build_p3_r1 = ConvRelu(network, weightMap, *block1->getOutput(0), 256, 1, 1, "build_feature_pyramid/build_P3/reduce_dimension/"); + assert(build_p3_r1); + + IResizeLayer *bfp_layer3_resize = network->addResize(*build_p4_r2->getOutput(0)); + bfp_layer3_resize->setResizeMode(ResizeMode::kNEAREST); + auto build_p3_r1_shape = network->addShape(*build_p3_r1->getOutput(0))->getOutput(0); + bfp_layer3_resize->setInput(1, *build_p3_r1_shape); + bfp_layer3_resize->setAlignCorners(false); + assert(bfp_layer3_resize); + IElementWiseLayer *bfp_add1 = network->addElementWise(*bfp_layer3_resize->getOutput(0), *build_p3_r1->getOutput(0), ElementWiseOperation::kSUM); + assert(bfp_add1); + + IActivationLayer *build_p3_r2 = ConvRelu(network, weightMap, *bfp_add1->getOutput(0), 256, 3, 1, "build_feature_pyramid/build_P3/avoid_aliasing/"); + assert(build_p3_r2); + + IActivationLayer *build_p2_r1 = ConvRelu(network, weightMap, *pool1->getOutput(0), 256, 1, 1, "build_feature_pyramid/build_P2/reduce_dimension/"); + assert(build_p2_r1); + IResizeLayer *bfp_layer2_resize = network->addResize(*build_p3_r2->getOutput(0)); + bfp_layer2_resize->setResizeMode(ResizeMode::kNEAREST); + auto build_p2_r1_shape = network->addShape(*build_p2_r1->getOutput(0))->getOutput(0); + bfp_layer2_resize->setInput(1, *build_p2_r1_shape); + bfp_layer2_resize->setAlignCorners(false); + assert(bfp_layer2_resize); + IElementWiseLayer *bfp_add2 = network->addElementWise(*bfp_layer2_resize->getOutput(0), *build_p2_r1->getOutput(0), ElementWiseOperation::kSUM); + assert(bfp_add2); + + // P2 + IActivationLayer *build_p2_r2 = ConvRelu(network, weightMap, *bfp_add2->getOutput(0), 256, 3, 1, "build_feature_pyramid/build_P2/avoid_aliasing/"); + assert(build_p2_r2); + auto build_p2_r2_shape = network->addShape(*build_p2_r2->getOutput(0))->getOutput(0); + // P3 x2 + IResizeLayer *layer1_resize = network->addResize(*build_p3_r2->getOutput(0)); + layer1_resize->setResizeMode(ResizeMode::kLINEAR); + layer1_resize->setInput(1, *build_p2_r2_shape); + layer1_resize->setAlignCorners(true); + assert(layer1_resize); + + // P4 x4 + IResizeLayer *layer2_resize = network->addResize(*build_p4_r2->getOutput(0)); + layer2_resize->setResizeMode(ResizeMode::kLINEAR); + layer2_resize->setInput(1, *build_p2_r2_shape); + layer2_resize->setAlignCorners(true); + + assert(layer2_resize); + + // P5 x8 + IResizeLayer *layer3_resize = network->addResize(*build_p5_r1->getOutput(0)); + layer3_resize->setResizeMode(ResizeMode::kLINEAR); + layer3_resize->setInput(1, *build_p2_r2_shape); + layer3_resize->setAlignCorners(true); + assert(layer3_resize); + + // C(P5,P4,P3,P2) + ITensor *inputTensors[] = {layer3_resize->getOutput(0), layer2_resize->getOutput(0), layer1_resize->getOutput(0), build_p2_r2->getOutput(0)}; + + IConcatenationLayer *concat = network->addConcatenation(inputTensors, 4); + assert(concat); + + IConvolutionLayer *feature_result_conv = network->addConvolutionNd(*concat->getOutput(0), 256, DimsHW{3, 3}, weightMap["feature_results/Conv/weights"], emptywts); + feature_result_conv->setPaddingNd(DimsHW{1, 1}); + assert(feature_result_conv); + + IScaleLayer *feature_result_bn = addBatchNorm2d(network, weightMap, *feature_result_conv->getOutput(0), "feature_results/Conv/BatchNorm/", 1e-5); + assert(feature_result_bn); + + IActivationLayer *feature_result_relu = network->addActivation(*feature_result_bn->getOutput(0), ActivationType::kRELU); + assert(feature_result_relu); + IConvolutionLayer *feature_result_conv_1 = network->addConvolutionNd(*feature_result_relu->getOutput(0), 6, DimsHW{1, 1}, weightMap["feature_results/Conv_1/weights"], weightMap["feature_results/Conv_1/biases"]); + assert(feature_result_conv_1); + + IActivationLayer *sigmoid = network->addActivation(*feature_result_conv_1->getOutput(0), ActivationType::kSIGMOID); + assert(sigmoid); + + sigmoid->getOutput(0)->setName(output_name_); + std::cout << "Set name out" << std::endl; + network->markOutput(*sigmoid->getOutput(0)); + + // Set profile + IOptimizationProfile *profile = builder->createOptimizationProfile(); + profile->setDimensions(input_name_, OptProfileSelector::kMIN, Dims4(1, 3, MIN_INPUT_SIZE, MIN_INPUT_SIZE)); + profile->setDimensions(input_name_, OptProfileSelector::kOPT, Dims4(1, 3, OPT_INPUT_H, OPT_INPUT_W)); + profile->setDimensions(input_name_, OptProfileSelector::kMAX, Dims4(1, 3, MAX_INPUT_SIZE, MAX_INPUT_SIZE)); + config->addOptimizationProfile(profile); + + // Build engine + config->setMaxWorkspaceSize(16 * (1 << 20)); // 16MB +#ifdef USE_FP16 + config->setFlag(BuilderFlag::kFP16); +#endif + ICudaEngine *engine = builder->buildEngineWithConfig(*network, *config); + ; + std::cout << "Build out" << std::endl; + + // Don't need the network any more + network->destroy(); + + // Release host memory + for (auto &mem : weightMap) + { + free((void *)(mem.second.values)); + } + return engine; +} + +void PSENet::serializeEngine() +{ + // Create builder + IBuilder *builder = createInferBuilder(gLogger); + IBuilderConfig *config = builder->createBuilderConfig(); + // Create model to populate the network, then set the outputs and create an engine + ICudaEngine *engine = createEngine(builder, config); + assert(engine != nullptr); + + // Serialize the engine + IHostMemory *modelStream{nullptr}; + modelStream = engine->serialize(); + assert(modelStream != nullptr); + + std::ofstream p("./psenet.engine", std::ios::binary | std::ios::out); + if (!p) + { + std::cerr << "Could not open plan output file" << std::endl; + return; + } + p.write(reinterpret_cast(modelStream->data()), modelStream->size()); + + return; +} + +void PSENet::deserializeEngine() +{ + std::ifstream file("./psenet.engine", std::ios::binary | std::ios::in); + if (file.good()) + { + file.seekg(0, file.end); + size_t size = file.tellg(); + file.seekg(0, file.beg); + char *trtModelStream = new char[size]; + assert(trtModelStream); + file.read(trtModelStream, size); + file.close(); + mCudaEngine = std::shared_ptr(mRuntime->deserializeCudaEngine(trtModelStream, size), InferDeleter()); + assert(mCudaEngine != nullptr); + } +} + +void PSENet::inferenceOnce(IExecutionContext &context, float *input, float *output, int input_h, int input_w) +{ + const ICudaEngine &engine = context.getEngine(); + // Pointers to input and output device buffers to pass to engine. + // Engine requires exactly IEngine::getNbBindings() number of buffers. + assert(engine.getNbBindings() == 2); + void *buffers[2]; + + // In order to bind the buffers, we need to know the names of the input and output tensors. + // Note that indices are guaranteed to be less than IEngine::getNbBindings() + const int inputIndex = engine.getBindingIndex(input_name_); + const int outputIndex = engine.getBindingIndex(output_name_); + + context.setBindingDimensions(inputIndex, Dims4(1, 3, input_h, input_w)); + + int input_size = 3 * input_h * input_w * sizeof(float); + int output_size = input_h * input_w * 6 / 16 * sizeof(float); + + // Create GPU buffers on device + CHECK(cudaMalloc(&buffers[inputIndex], input_size)); + CHECK(cudaMalloc(&buffers[outputIndex], output_size)); + + // Create stream + cudaStream_t stream; + CHECK(cudaStreamCreate(&stream)); + + // DMA input batch data to device, infer on the batch asynchronously, and DMA output back to host + CHECK(cudaMemcpyAsync(buffers[inputIndex], input, input_size, cudaMemcpyHostToDevice, stream)); + context.enqueueV2(buffers, stream, nullptr); + CHECK(cudaMemcpyAsync(output, buffers[outputIndex], output_size, cudaMemcpyDeviceToHost, stream)); + cudaStreamSynchronize(stream); + + // Release stream and buffers + cudaStreamDestroy(stream); + CHECK(cudaFree(buffers[inputIndex])); + CHECK(cudaFree(buffers[outputIndex])); +} + +void PSENet::init() +{ + mRuntime = std::shared_ptr(createInferRuntime(gLogger), InferDeleter()); + assert(mRuntime != nullptr); + + std::cout << "Deserialize Engine" << std::endl; + deserializeEngine(); + + mContext = std::shared_ptr(mCudaEngine->createExecutionContext(), InferDeleter()); + assert(mContext != nullptr); + + mContext->setOptimizationProfile(0); + + std::cout << "Finished init" << std::endl; +} +void PSENet::detect(std::string image_path) +{ + + int batch_size = 1; + + // Run inference + + cv::Mat image = cv::imread(image_path); + int resize_h, resize_w; + float ratio_h, ratio_w; + + auto start = std::chrono::system_clock::now(); + + float *input = preProcess(image, resize_h, resize_w, ratio_h, ratio_w); + float *output = new float[resize_h * resize_w * 6 / 16]; + + inferenceOnce(*mContext, input, output, resize_h, resize_w); + + cv::Mat mask; + postProcess(output, mask, resize_h, resize_w); + + drawRects(image, mask, ratio_h, ratio_w, stride_, 1.4); + auto end = std::chrono::system_clock::now(); + + cv::imwrite("result_" + image_path, image); + + std::cout << std::chrono::duration_cast(end - start).count() << "ms" << std::endl; +} + +float *PSENet::preProcess(cv::Mat image, int &resize_h, int &resize_w, float &ratio_h, float &ratio_w) +{ + cv::Mat imageRGB; + cv::cvtColor(image, imageRGB, CV_BGR2RGB); + cv::Mat imageProcessed; + int h = imageRGB.size().height; + int w = imageRGB.size().width; + resize_w = w; + resize_h = h; + + float ratio = 1.0; + // limit the max side + if (resize_h > max_side_len_ && resize_w > max_side_len_) + { + if (resize_h > resize_w) + { + ratio = float(max_side_len_) / float(resize_h); + } + else + { + ratio = float(max_side_len_) / float(resize_w); + } + } + resize_h = int(resize_h * ratio); + resize_w = int(resize_w * ratio); + + if (resize_h % 32 != 0) + { + resize_h = (resize_h / 32 + 1) * 32; + } + if (resize_w % 32 != 0) + { + resize_w = (resize_w / 32 + 1) * 32; + } + ratio_h = resize_h / float(h); + ratio_w = resize_w / float(w); + + cv::resize(imageRGB, imageProcessed, cv::Size(resize_w, resize_h)); + float *input = new float[3 * resize_h * resize_w]; + cv::Mat imgFloat; + imageProcessed.convertTo(imgFloat, CV_32FC3); + cv::subtract(imgFloat, cv::Scalar(123.68, 116.78, 103.94), imgFloat, cv::noArray(), -1); + std::vector chw; + for (auto i = 0; i < 3; ++i) + { + chw.emplace_back(cv::Mat(cv::Size(resize_w, resize_h), CV_32FC1, input + i * resize_w * resize_h)); + } + cv::split(imgFloat, chw); + return input; +} + +void PSENet::postProcess(float *origin_output, cv::Mat &label_image, int resize_h, int resize_w) +{ + // BxCxHxW S0 ===> S5 small ===> large + const int height = (resize_h + stride_ - 1) / stride_; + const int width = (resize_w + stride_ - 1) / stride_; + const int length = height * width; + + std::vector kernels(num_kernels_); + cv::Mat max_kernel(height, width, CV_32F, (void *)(origin_output + (num_kernels_ - 1) * length), 0); + cv::threshold(max_kernel, max_kernel, post_threshold_, 255, cv::THRESH_BINARY); + max_kernel.convertTo(max_kernel, CV_8U); + assert(max_kernel.rows == height && max_kernel.cols == width); + for (auto i = 0; i < num_kernels_ - 1; ++i) + { + cv::Mat kernel = cv::Mat(height, width, CV_32F, (void *)(origin_output + i * length), 0); + cv::threshold(kernel, kernel, post_threshold_, 255, cv::THRESH_BINARY); + kernel.convertTo(kernel, CV_8U); + cv::bitwise_and(kernel, max_kernel, kernel); + assert(kernel.rows == height && kernel.cols == width); + kernels[i] = kernel; + } + kernels[num_kernels_ - 1] = max_kernel; + + cv::Mat stats, centroids; + int num_labels = cv::connectedComponentsWithStats(kernels[0], label_image, stats, centroids, 4); + label_image.convertTo(label_image, CV_8U); + assert(label_image.rows == max_kernel.rows && label_image.cols == max_kernel.cols); + + std::map> contourMaps; + + // PSE algorithm + std::queue> q; + std::queue> q_next; + for (auto h = 0; h < height; ++h) + { + for (auto w = 0; w < width; ++w) + { + auto label = *label_image.ptr(h, w); + if (label > 0) + { + q.emplace(std::make_tuple(w, h, label)); + contourMaps[label].emplace_back(cv::Point(w, h)); + } + } + } + int dx[4] = {-1, 1, 0, 0}; + int dy[4] = {0, 0, -1, 1}; + for (auto idx = 1; idx < num_kernels_; ++idx) + { + auto *ptr_kernel = kernels[idx].data; + while (!q.empty()) + { + auto q_n = q.front(); + q.pop(); + int x = std::get<0>(q_n); + int y = std::get<1>(q_n); + int l = std::get<2>(q_n); + bool is_edge = true; + for (auto j = 0; j < 4; ++j) + { + int tmpx = x + dx[j]; + int tmpy = y + dy[j]; + int offset = tmpy * width + tmpx; + if (tmpx < 0 || tmpx >= width || tmpy < 0 || tmpy >= height) + { + continue; + } + if (!(int)ptr_kernel[offset] || (int)*label_image.ptr(tmpy, tmpx) > 0) + { + continue; + } + q.emplace(std::make_tuple(tmpx, tmpy, l)); + *label_image.ptr(tmpy, tmpx) = l; + contourMaps[l].emplace_back(cv::Point(tmpx, tmpy)); + is_edge = false; + } + if (is_edge) + { + q_next.emplace(std::make_tuple(x, y, l)); + } + } + std::swap(q, q_next); + } +} diff --git a/psenet/psenet.h b/psenet/psenet.h new file mode 100644 index 0000000..fe44369 --- /dev/null +++ b/psenet/psenet.h @@ -0,0 +1,39 @@ +#ifndef TENSORRTX_PSENET_H +#define TENSORRTX_PSENET_H +#include +#include +#include +#include +#include "utils.h" +#include "layers.h" + +class PSENet +{ +public: + PSENet(int max_side_len, float threshold, int num_kernel, int stride); + ~PSENet(); + + ICudaEngine *createEngine(IBuilder *builder, IBuilderConfig *config); + void serializeEngine(); + void deserializeEngine(); + void init(); + void inferenceOnce(IExecutionContext &context, float *input, float *output, int input_h, int input_w); + void detect(std::string image_path); + float *preProcess(cv::Mat image, int &resize_h, int &resize_w, float &ratio_h, float &ratio_w); + void postProcess(float *origin_output, cv::Mat &label_image, int resize_h, int resize_w); + +private: + Logger gLogger; + std::shared_ptr mRuntime; + std::shared_ptr mCudaEngine; + std::shared_ptr mContext; + DataType dt = DataType::kFLOAT; + const char *input_name_ = "input"; + const char *output_name_ = "maps"; + int max_side_len_ = 640; + float post_threshold_ = 0.9; + int num_kernels_ = 6; + int stride_ = 4; +}; + +#endif // TENSORRTX_PSENET_H diff --git a/psenet/test.jpg b/psenet/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6d3028a40e0931345c0eed77a56bb09ee4838674 GIT binary patch literal 43075 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 +std::map loadWeights(const std::string file) +{ + std::cout << "Loading weights: " << file << std::endl; + std::cout << "Model weight is large, it will take some time." << std::endl; + std::map weightMap; + + // Open weights file + std::ifstream input(file); + assert(input.is_open() && "Unable to load weight file."); + + // Read number of weight blobs + int32_t count; + input >> count; + assert(count > 0 && "Invalid weight map file."); + + while (count--) + { + Weights wt{DataType::kFLOAT, nullptr, 0}; + uint32_t size; + + // Read name and type of blob + std::string name; + input >> name >> std::dec >> size; + wt.type = DataType::kFLOAT; + + // Load blob + uint32_t *val = reinterpret_cast(malloc(sizeof(val) * size)); + for (uint32_t x = 0, y = size; x < y; ++x) + { + input >> std::hex >> val[x]; + } + wt.values = val; + + wt.count = size; + weightMap[name] = wt; + } + std::cout << "Finish load weight" << std::endl; + return weightMap; +} + +cv::RotatedRect expandBox(const cv::RotatedRect &inBox, float ratio) +{ + cv::Size size = inBox.size; + int neww = int(size.width * ratio); + int newh = int(size.height * ratio); + return cv::RotatedRect(inBox.center, cv::Size(neww, newh), inBox.angle); +} + +void drawRects(cv::Mat &image, cv::Mat mask, float ratio_h, float ratio_w, int stride, float expand_ratio) +{ + std::vector> contours; + std::vector hierarcy; + cv::findContours(mask, contours, hierarcy, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE); + + std::vector boundRect(contours.size()); + std::vector box(contours.size()); + cv::Point2f rect[4]; + for (auto i = 0; i < contours.size(); i++) + { + box[i] = cv::minAreaRect(cv::Mat(contours[i])); + cv::RotatedRect expandbox = expandBox(box[i], expand_ratio); + expandbox.points(rect); + for (auto j = 0; j < 4; j++) + { + cv::line(image, cv::Point{int(rect[j].x / ratio_w * stride), int(rect[j].y / ratio_h * stride)}, cv::Point{int(rect[(j + 1) % 4].x / ratio_w * stride), int(rect[(j + 1) % 4].y / ratio_h * stride)}, cv::Scalar(0, 0, 255), 2, 8); + } + } +} diff --git a/psenet/utils.h b/psenet/utils.h new file mode 100644 index 0000000..77d99d0 --- /dev/null +++ b/psenet/utils.h @@ -0,0 +1,82 @@ +#ifndef TENSORRTX_UTILS_H +#define TENSORRTX_UTILS_H + +#include +#include +#include "NvInfer.h" +#include "cuda_runtime_api.h" +#include "assert.h" + +using namespace nvinfer1; + +std::map loadWeights(const std::string file); + +cv::RotatedRect expandBox(const cv::RotatedRect &inBox, float ratio = 1.0); + +void drawRects(cv::Mat &image, cv::Mat mask, float ratio_h, float ratio_w, int stride, float expand_ratio = 1.4); + +cv::Mat renderSegment(cv::Mat image, const cv::Mat &mask); + +// <============== Operator =============> +struct InferDeleter +{ + template + void operator()(T *obj) const + { + if (obj) + { + obj->destroy(); + } + } +}; + +#define CHECK(status) \ + do \ + { \ + auto ret = (status); \ + if (ret != 0) \ + { \ + std::cout << "Cuda failure: " << ret; \ + abort(); \ + } \ + } while (0) + +// Logger for TensorRT info/warning/errors +class Logger : public nvinfer1::ILogger +{ +public: + Logger() : Logger(Severity::kWARNING) {} + + Logger(Severity severity) : reportableSeverity(severity) {} + + void log(Severity severity, const char *msg) override + { + // suppress messages with severity enum value greater than the reportable + if (severity > reportableSeverity) + return; + + switch (severity) + { + case Severity::kINTERNAL_ERROR: + std::cerr << "INTERNAL_ERROR: "; + break; + case Severity::kERROR: + std::cerr << "ERROR: "; + break; + case Severity::kWARNING: + std::cerr << "WARNING: "; + break; + case Severity::kINFO: + std::cerr << "INFO: "; + break; + default: + std::cerr << "UNKNOWN: "; + break; + } + std::cerr << msg << std::endl; + } + + Severity reportableSeverity{Severity::kWARNING}; +}; + +#endif