Yolov7_tiny post processing

Dear Hailo Community,
I’m trying to run yolov7_tiny.hef from the model explorer in a gstreamer pipeline on i.mx 8m Plus.
I built a small gstreamer pipeline inspired by detection.sh (tappas/apps/h8/gstreamer/general/detection/detection.sh at master · hailo-ai/tappas · GitHub) to check if the model is working.
This is my Pipeline:

$ gst-launch-1.0 -e videotestsrc ! hailonet hef-path=model/yolov7_tiny.hef is-active=true ! hailofilter so-path=/usr/lib/hailo-post-processes/libyolo_post.so config-path=model/yolov7.json function-name=yolov5 qos=false ! fakesink

which returns

Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
terminate called after throwing an instance of 'std::runtime_error'
  what():  config class labels do not match output tensors! config labels size: 80 tensors num classes: 4294967291

Aborted

yolov7.json came from here: Hailo-Application-Code-Examples/runtime/cpp/detection/yolov5_yolov7_detection/yolov7.json at b23e4f3454247900e634bf8be638683c723d5ccb · hailo-ai/Hailo-Application-Code-Examples · GitHub

Running the pipeline without hailofilter or the model with $ hailortcli run model/yolov7_tiny.hef works fine. So this should be a post processing issue.

I had a look at yolo_postprocessing.cpp (tappas/core/hailo/libs/postprocesses/detection/yolo_postprocess.cpp at master · hailo-ai/tappas · GitHub) which is compiled into libyolo_post.so and added a few debug lines in order to take a look at the numbers and shapes of output Tensors.
No. of output Tensors = 1 ( which to my understanding is expected because of the use of yolov5_nms_postprocess layer )
Width = 80, Height = 80, Features = 0 ( width and height are expected, but features should be 5(?) according to the output shape displayed by hailo profiler )

And here is the output of

$ hailortcli parse-hef --parse-streams --parse-vstreams model/yolov7_tiny.hef
Architecture HEF was compiled for: HAILO8
Network group name: yolov7_tiny, Single Context
    Network name: yolov7_tiny/yolov7_tiny
        Stream infos:
            Input  yolov7_tiny/input_layer1 UINT8, NHCW(640x640x3)
            Output yolov7_tiny/conv58_82 UINT8, NHCW(20x24x255)
            Output yolov7_tiny/conv51_82 UINT8, NHCW(40x40x255)
            Output yolov7_tiny/conv43_82 UINT8, NHCW(80x80x255)
        VStream infos:
            Input  yolov7_tiny/input_layer1 UINT8, NHWC(640x640x3)
            Output yolov7_tiny/yolov5_nms_postprocess FLOAT32, HAILO NMS(number of classes: 80, maximum bounding boxes per class: 80, maximum frame size: 128320)
            Operation:
                Op YOLOV5
                Name: YOLOv5-Post-Process
                Score threshold: 0.200
                IoU threshold: 0.60
                Classes: 80
                Cross classes: false
                Max bboxes per class: 80
                Image height: 640.00
                Image width: 640.00

which looks good.

My Versions:
HailoRT-CLI version 4.16.2
Tappas 3.27.2 (I tried the master branch as well, no success)
hailo-fimware Version: 4.16.2
libhailort 4.16.2

Help would be much appreciated.

Tested again with Versions:
HailoRT-CLI version 4.17.1
Tappas 3.28.1 (I tried the master branch as well, no success)
hailo-fimware Version: 4.17.1
libhailort 4.17.1

same result

Hi,
It looks like your HEF is using HailoRT Post Process layer.
So NMS is handled by HailoRT as part of the HEF.
However you are using the libyolo_post.so which is build to use HEFs w/o this post process.
You need to use: libyolo_hailortpp_post.so
Note that the version in the current TAPPAS might not support your HEF out of the box.
See updated post process we released in our RPI 5 examples repo:

This will be officially merged to TAPPAS in the upcoming release.

@giladn thanks so much for the pointer. I went ahead and added a yolov7_tiny function in yolo_hailortpp.cpp and added the output_layer_name as an optional paramter.

Here’s the code for anyone with the same issue. I dont know your policies on code contribution, but if you would like me to open a pull request in tappas I’d be happy to.

yolo_hailortpp.hpp

/**
* Copyright (c) 2021-2022 Hailo Technologies Ltd. All rights reserved.
* Distributed under the LGPL license (https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt)
**/
#pragma once
#include "hailo_objects.hpp"
#include "hailo_common.hpp"

__BEGIN_DECLS

class YoloParamsNMS
{
public:
    std::map<std::uint8_t, std::string> labels;
    float detection_threshold;
    uint max_boxes;
    bool filter_by_score=false;
    std::string output_layer_name;
    YoloParamsNMS(std::map<uint8_t, std::string> dataset = std::map<uint8_t, std::string>(),
                  float detection_threshold = 0.3f,
                  uint max_boxes = 200)
        : labels(dataset),
          detection_threshold(detection_threshold), 
          max_boxes(max_boxes) {}
};

YoloParamsNMS *init(const std::string config_path, const std::string function_name);
void free_resources(void *params_void_ptr);
void filter(HailoROIPtr roi, void *params_void_ptr);
void filter_letterbox(HailoROIPtr roi, void *params_void_ptr);
void yolov5(HailoROIPtr roi);
void yolov5s_nv12(HailoROIPtr roi);
void yolov7tiny(HailoROIPtr roi, void *params_void_ptr);
void yolov8s(HailoROIPtr roi);
void yolov8m(HailoROIPtr roi);
void yolox(HailoROIPtr roi);
void yolov5s_personface(HailoROIPtr roi);
void yolov5_no_persons(HailoROIPtr roi);
void yolov5m_vehicles(HailoROIPtr roi);
void yolov5m_vehicles_nv12(HailoROIPtr roi);
__END_DECLS

and yolo_hailortpp.cpp

#include <regex>
#include <fstream>
#include <sstream>
#include <map>
#include "rapidjson/document.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/error/en.h"
#include "rapidjson/filereadstream.h"
#include "rapidjson/schema.h"
#include "json_config.hpp"
#include "common/labels/coco_eighty.hpp"
#include "hailo_nms_decode.hpp"
#include "yolo_hailortpp.hpp"

static const std::string DEFAULT_YOLOV5S_OUTPUT_LAYER = "yolov5s_nv12/yolov5_nms_postprocess";
static const std::string DEFAULT_YOLOV5M_OUTPUT_LAYER = "yolov5m_wo_spp_60p/yolov5_nms_postprocess";
static const std::string DEFAULT_YOLOV5M_VEHICLES_OUTPUT_LAYER = "yolov5m_vehicles/yolov5_nms_postprocess";
static const std::string DEFAULT_YOLOV8S_OUTPUT_LAYER = "yolov8s/yolov8_nms_postprocess";
static const std::string DEFAULT_YOLOV8M_OUTPUT_LAYER = "yolov8m/yolov8_nms_postprocess";
static const std::string DEFAULT_YOLOV7TINY_OUTPUT_LAYER = "yolov7_tiny/yolov5_nms_postprocess";

#if __GNUC__ > 8
#include <filesystem>
namespace fs = std::filesystem;
#else
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#endif

YoloParamsNMS *init(const std::string config_path, const std::string function_name)
{
    YoloParamsNMS *params;
    if (!fs::exists(config_path))
    {
        params = new YoloParamsNMS(common::coco_eighty);
        return params;
    }
    else
    {
        params = new YoloParamsNMS();
        char config_buffer[4096];
        const char *json_schema = R""""({
        "$schema": "http://json-schema.org/draft-04/schema#",
        "type": "object",
        "properties": {
            "detection_threshold": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
            },
            "max_boxes": {
            "type": "integer"
            },
            "labels": {
            "type": "array",
            "items": {
                "type": "string"
                }
            },
            "output_layer_name": {
            "type": "string"
            }
        },
        "required": [
            "labels"
        ]
        })"""";

        std::FILE *fp = fopen(config_path.c_str(), "r");
        if (fp == nullptr)
        {
            throw std::runtime_error("JSON config file is not valid");
        }
        rapidjson::FileReadStream stream(fp, config_buffer, sizeof(config_buffer));
        bool valid = common::validate_json_with_schema(stream, json_schema);
        if (valid)
        {
            rapidjson::Document doc_config_json;
            doc_config_json.ParseStream(stream);

            // parse labels
            auto labels = doc_config_json["labels"].GetArray();
            uint i = 0;
            for (auto &v : labels)
            {
                params->labels.insert(std::pair<std::uint8_t, std::string>(i, v.GetString()));
                i++;
            }

            // set the params
            if (doc_config_json.HasMember("detection_threshold")) {
                params->detection_threshold = doc_config_json["detection_threshold"].GetFloat();
            }
            if (doc_config_json.HasMember("max_boxes")) {
                params->max_boxes = doc_config_json["max_boxes"].GetInt();
                params->filter_by_score = true;
            }
            if (doc_config_json.HasMember("output_layer_name")) {
                params->output_layer_name = doc_config_json["output_layer_name"].GetString();
            }
        }
        fclose(fp);
    }
    return params;
}
void free_resources(void *params_void_ptr)
{
    YoloParamsNMS *params = reinterpret_cast<YoloParamsNMS *>(params_void_ptr);
    delete params;
}

static std::map<uint8_t, std::string> yolo_vehicles_labels = {
    {0, "unlabeled"},
    {1, "car"}};
static std::map<uint8_t, std::string> yolo_personface = {
        {0, "unlabeled"},
        {1, "person"},
        {2, "face"}};

void yolov5(HailoROIPtr roi)
{
    if (!roi->has_tensors())
    {
        return;
    }
    auto post = HailoNMSDecode(roi->get_tensor(DEFAULT_YOLOV5M_OUTPUT_LAYER), common::coco_eighty);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolov5s_nv12(HailoROIPtr roi)
{
    if (!roi->has_tensors())
    {
        return;
    }
    auto post = HailoNMSDecode(roi->get_tensor(DEFAULT_YOLOV5S_OUTPUT_LAYER), common::coco_eighty);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolov8s(HailoROIPtr roi)
{
    if (!roi->has_tensors())
    {
        return;
    }
    auto post = HailoNMSDecode(roi->get_tensor(DEFAULT_YOLOV8S_OUTPUT_LAYER), common::coco_eighty);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolov8m(HailoROIPtr roi)
{
    if (!roi->has_tensors())
    {
        return;
    }
    auto post = HailoNMSDecode(roi->get_tensor(DEFAULT_YOLOV8M_OUTPUT_LAYER), common::coco_eighty);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolox(HailoROIPtr roi)
{
    auto post = HailoNMSDecode(roi->get_tensor("yolox_nms_postprocess"), common::coco_eighty);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolov5m_vehicles(HailoROIPtr roi)
{
    auto post = HailoNMSDecode(roi->get_tensor(DEFAULT_YOLOV5M_VEHICLES_OUTPUT_LAYER), yolo_vehicles_labels);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolov5m_vehicles_nv12(HailoROIPtr roi)
{
    if (!roi->has_tensors())
    {
        return;
    }
    auto post = HailoNMSDecode(roi->get_tensor("yolov5m_vehicles_nv12/yolov5_nms_postprocess"), yolo_vehicles_labels);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolov5s_personface(HailoROIPtr roi)
{
    if (!roi->has_tensors())
    {
        return;
    }
    auto post = HailoNMSDecode(roi->get_tensor("yolov5s_personface_nv12/yolov5_nms_postprocess"), yolo_personface);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    hailo_common::add_detections(roi, detections);
}

void yolov5_no_persons(HailoROIPtr roi)
{
    auto post = HailoNMSDecode(roi->get_tensor(DEFAULT_YOLOV5M_OUTPUT_LAYER), common::coco_eighty);
    auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
    for (auto it = detections.begin(); it != detections.end();)
    {
        if (it->get_label() == "person")
        {
            it = detections.erase(it);
        }
        else
        {
            ++it;
        }
    }
    hailo_common::add_detections(roi, detections);
}

void yolov7tiny(HailoROIPtr roi, void *params_void_ptr)
{
    if (!roi->has_tensors())
    {
        return;
    }
    YoloParamsNMS *params = reinterpret_cast<YoloParamsNMS *>(params_void_ptr);
    std::vector<HailoTensorPtr> tensors = roi->get_tensors();
    // find the nms tensor
    for (auto tensor : tensors)
    {
        if (std::regex_search(tensor->name(), std::regex("nms_postprocess"))) 
        {
            std::string output_layer_name;
            if (params->output_layer_name.empty()){
                output_layer_name = DEFAULT_YOLOV7TINY_OUTPUT_LAYER;
            } else {
                output_layer_name = params->output_layer_name;
            }

            auto post = HailoNMSDecode(roi->get_tensor(output_layer_name), params->labels, params->detection_threshold, params->max_boxes, params->filter_by_score);
            auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
            hailo_common::add_detections(roi, detections);
        }
    }
}

void filter(HailoROIPtr roi, void *params_void_ptr)
{
    if (!roi->has_tensors())
    {
        return;
    }
    YoloParamsNMS *params = reinterpret_cast<YoloParamsNMS *>(params_void_ptr);
    std::vector<HailoTensorPtr> tensors = roi->get_tensors();
    // find the nms tensor
    for (auto tensor : tensors)
    {
        if (std::regex_search(tensor->name(), std::regex("nms_postprocess"))) 
        {
            auto post = HailoNMSDecode(tensor, params->labels, params->detection_threshold, params->max_boxes, params->filter_by_score);
            auto detections = post.decode<float32_t, common::hailo_bbox_float32_t>();
            hailo_common::add_detections(roi, detections);
        }
    }
}
void filter_letterbox(HailoROIPtr roi, void *params_void_ptr)
{
    filter(roi, params_void_ptr);
    // Resize Letterbox
    HailoBBox roi_bbox = hailo_common::create_flattened_bbox(roi->get_bbox(), roi->get_scaling_bbox());
    auto detections = hailo_common::get_hailo_detections(roi);
    for (auto &detection : detections)
    {
        auto detection_bbox = detection->get_bbox();
        auto xmin = (detection_bbox.xmin() * roi_bbox.width()) + roi_bbox.xmin();
        auto ymin = (detection_bbox.ymin() * roi_bbox.height()) + roi_bbox.ymin();
        auto xmax = (detection_bbox.xmax() * roi_bbox.width()) + roi_bbox.xmin();
        auto ymax = (detection_bbox.ymax() * roi_bbox.height()) + roi_bbox.ymin();

        HailoBBox new_bbox(xmin, ymin, xmax - xmin, ymax - ymin);
        detection->set_bbox(new_bbox);
    }

    // Clear the scaling bbox of main roi because all detections are fixed.
    roi->clear_scaling_bbox();

}
2 Likes

Great news!

We have added a regular expression to the default function filter. This function is called if no function-name input is provided. You should be able to use it as is to support many networks without needing to update the function name.

I really appreciate your suggestion to actively contribute to our code. In this case, I think it is not required, and we prefer users to use the default function (the rest of the functions are there for backward compatibility).
I will be happy to see your contributions in the future!
:wink:

1 Like