Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (C) 2021-2025 Intel Corporation
* Copyright (C) 2021-2026 Intel Corporation
*
* SPDX-License-Identifier: MIT
******************************************************************************/
Expand Down Expand Up @@ -45,26 +45,7 @@ BlobToMetaConverter::Ptr BlobToTensorConverter::create(BlobToMetaConverter::Init
else if (converter_name == PaddleOCRConverter::getName())
return std::make_unique<PaddleOCRConverter>(std::move(initializer));
else if (converter_name == DetectionAnomalyConverter::getName()) {

auto &model_proc_output_info = initializer.model_proc_output_info;
if (model_proc_output_info == nullptr)
throw std::runtime_error("model_proc_output_info has not been properly initialized.");

double normalization_scale, image_threshold;
if (!gst_structure_get_double(model_proc_output_info.get(), "normalization_scale", &normalization_scale))
throw std::runtime_error("<rt_info><model_info> normalization_scale parameter undefined");
if (!gst_structure_get_double(model_proc_output_info.get(), "image_threshold", &image_threshold))
throw std::runtime_error("<rt_info><model_info> image_threshold parameter undefined");

const char *anomaly_detection_task_cstr =
gst_structure_get_string(model_proc_output_info.get(), "anomaly_task");
std::string anomaly_detection_task = anomaly_detection_task_cstr ? anomaly_detection_task_cstr : "";
if (anomaly_detection_task != DEFAULT_ANOMALY_DETECTION_TASK)
throw std::runtime_error("<rt_info><model_info> parameter anomaly_task definition error: only "
"'classification' is currently supported.");

return std::make_unique<DetectionAnomalyConverter>(std::move(initializer), image_threshold, normalization_scale,
anomaly_detection_task);
return std::make_unique<DetectionAnomalyConverter>(std::move(initializer));
}

throw std::runtime_error("ToTensorConverter \"" + converter_name + "\" is not implemented.");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (C) 2025 Intel Corporation
* Copyright (C) 2025-2026 Intel Corporation
*
* SPDX-License-Identifier: MIT
******************************************************************************/
Expand Down Expand Up @@ -31,10 +31,11 @@ TensorsTable DetectionAnomalyConverter::convert(const OutputBlobs &output_blobs)
tensors_table.resize(batch_size);

double pred_score = 0.0;
double image_threshold_norm = normalize(image_threshold, 0.0f);
cv::Mat anomaly_map;
cv::Mat anomaly_map_raw;
cv::Mat pred_mask;
std::string pred_label = "";
bool publish_pred_mask = false;

for (const auto &blob_iter : output_blobs) {
OutputBlob::Ptr blob = blob_iter.second;
Expand Down Expand Up @@ -62,32 +63,58 @@ TensorsTable DetectionAnomalyConverter::convert(const OutputBlobs &output_blobs)
const size_t img_height = dims[DEF_ANOMALY_TENSOR_LAYOUT_OFFSET_H];
const size_t img_width = dims[DEF_ANOMALY_TENSOR_LAYOUT_OFFSET_W];

anomaly_map = cv::Mat((int)img_height, (int)img_width, CV_32FC1, const_cast<float *>(data));
anomaly_map = anomaly_map / normalization_scale;
anomaly_map = cv::min(cv::max(anomaly_map, 0.f), 1.f);
// find the max predicted score
cv::minMaxLoc(anomaly_map, NULL, &pred_score);
anomaly_map_raw = cv::Mat((int)img_height, (int)img_width, CV_32FC1, const_cast<float *>(data));
// Clamping the anomaly map to [0, 1] range
anomaly_map_raw = cv::min(cv::max(anomaly_map_raw, 0.f), 1.f);
// find the the highest anomaly score in the anomaly map
cv::minMaxLoc(anomaly_map_raw, NULL, &pred_score);

const auto &labels = getLabels();
if (labels.size() != DEF_TOTAL_LABELS_COUNT)
throw std::runtime_error("Anomaly-detection converter: Expected 2 labels, got: " +
std::to_string(labels.size()));

pred_label = labels[pred_score > image_threshold_norm ? 1 : 0];
// classify using normalized threshold comparison
pred_label = labels[pred_score > (image_threshold) ? 1 : 0];

logParamsStats(pred_label, pred_score, image_threshold_norm);
// normalize the score to [0, 1] range using the provided normalization scale and image threshold
double pred_score_normalized = normalize(pred_score, image_threshold);
// invert score for Normal predictions
if (pred_label == "Normal") {
pred_score_normalized = 1.0 - pred_score_normalized;
}

// Log the parameters and statistics
logParamsStats(pred_label, pred_score_normalized, pred_score, image_threshold);

const std::string layer_name = blob_iter.first;
for (size_t frame_index = 0; frame_index < batch_size; ++frame_index) {
GVA::Tensor classification_result = createTensor();

classification_result.set_string("label", pred_label);
classification_result.set_double("confidence", pred_score);
classification_result.set_double("confidence", pred_score_normalized);

gst_structure_set(classification_result.gst_structure(), "tensor_id", G_TYPE_INT,
safe_convert<int>(frame_index), "type", G_TYPE_STRING, "classification_result",
"precision", G_TYPE_INT, static_cast<int>(blob->GetPrecision()), NULL);

// Add segmentation mask data if anomaly detected
if (pred_label == "Anomaly" && publish_pred_mask) {

// Create binary mask by thresholding the anomaly map with pixel_threshold
// anomaly_map_raw is already normalized to [0, 1], so the pixel_threshold can be directly applied

pred_mask = anomaly_map_raw >= pixel_threshold;
pred_mask.convertTo(pred_mask, CV_8U); // Convert to uint8 (0 and 255)

classification_result.set_format("segmentation_mask");
classification_result.set_dims(
{safe_convert<uint32_t>(pred_mask.cols), safe_convert<uint32_t>(pred_mask.rows)});
classification_result.set_precision(GVA::Tensor::Precision::U8);
classification_result.set_data(reinterpret_cast<const void *>(pred_mask.data),
pred_mask.rows * pred_mask.cols * sizeof(uint8_t));
}

std::vector<GstStructure *> tensors{classification_result.gst_structure()};
tensors_table[frame_index].push_back(tensors);
}
Expand All @@ -102,8 +129,8 @@ TensorsTable DetectionAnomalyConverter::convert(const OutputBlobs &output_blobs)
return tensors_table;
}

void DetectionAnomalyConverter::logParamsStats(const std::string &pred_label, const double &pred_score,
const double &image_threshold_norm) {
void DetectionAnomalyConverter::logParamsStats(const std::string &pred_label, const double &pred_score_normalized,
const double &pred_score, const double &image_threshold) {
if (pred_label == "Normal" || pred_label == "Anomaly") {
if (pred_label == "Normal") {
lbl_normal_cnt++;
Expand All @@ -116,8 +143,8 @@ void DetectionAnomalyConverter::logParamsStats(const std::string &pred_label, co
pred_label);
}

GVA_INFO("pred_label: %s, pred_score: %f, image_threshold: %f, "
"image_threshold_norm: %f, normalization_scale: %f, #normal: %u, #anomaly: %u",
pred_label.c_str(), pred_score, image_threshold, image_threshold_norm, normalization_scale, lbl_normal_cnt,
lbl_anomaly_cnt);
GVA_WARNING("pred_label: %s, pred_score_normalized: %f, pred_score: %f, image_threshold: %f, "
"normalization_scale: %f, #normal: %u, #anomaly: %u",
pred_label.c_str(), pred_score_normalized, pred_score, image_threshold, normalization_scale,
lbl_normal_cnt, lbl_anomaly_cnt);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (C) 2025 Intel Corporation
* Copyright (C) 2025-2026 Intel Corporation
*
* SPDX-License-Identifier: MIT
******************************************************************************/
Expand All @@ -20,7 +20,8 @@
namespace post_processing {

class DetectionAnomalyConverter : public BlobToTensorConverter {
double image_threshold;
double image_threshold; // Image threshold that is used for classifying an image as anomalous or normal
double pixel_threshold; // Pixel threshold that is used for segmenting anomalous regions in the image
double normalization_scale;
std::string anomaly_detection_task;
uint lbl_normal_cnt = 0;
Expand All @@ -34,20 +35,35 @@ class DetectionAnomalyConverter : public BlobToTensorConverter {
}

public:
DetectionAnomalyConverter(BlobToMetaConverter::Initializer initializer, double image_threshold,
double normalization_scale, std::string anomaly_detection_task = "")
: BlobToTensorConverter(std::move(initializer)), image_threshold(image_threshold),
normalization_scale(normalization_scale), anomaly_detection_task(std::move(anomaly_detection_task)) {
DetectionAnomalyConverter(BlobToMetaConverter::Initializer initializer)
: BlobToTensorConverter(std::move(initializer)) {
validateAndLoadParameters();
}

TensorsTable convert(const OutputBlobs &output_blobs) override;

static std::string getName() {
return "AnomalyDetection";
}

private:
void logParamsStats(const std::string &pred_label, const double &pred_score, const double &image_threshold_norm);
void validateAndLoadParameters() {
auto task_cstr = gst_structure_get_string(getModelProcOutputInfo().get(), "anomaly_task");
if (std::string(task_cstr ? task_cstr : "") != DEFAULT_ANOMALY_DETECTION_TASK)
throw std::runtime_error("<rt_info><model_info> parameter anomaly_task definition error: only "
"'classification' is currently supported.");

if (!gst_structure_get_double(getModelProcOutputInfo().get(), "normalization_scale", &normalization_scale))
throw std::runtime_error("<rt_info><model_info> normalization_scale parameter undefined");

if (!gst_structure_get_double(getModelProcOutputInfo().get(), "image_threshold", &image_threshold))
throw std::runtime_error("<rt_info><model_info> image_threshold parameter undefined");

if (!gst_structure_get_double(getModelProcOutputInfo().get(), "pixel_threshold", &pixel_threshold))
throw std::runtime_error("<rt_info><model_info> pixel_threshold parameter undefined");
}

void logParamsStats(const std::string &pred_label, const double &pred_score_normalized, const double &pred_score,
const double &image_threshold);
};

} // namespace post_processing
Original file line number Diff line number Diff line change
Expand Up @@ -746,14 +746,22 @@ std::map<std::string, GstStructure *> get_model_info_postproc(const std::shared_
GST_INFO("[get_model_info_postproc] normalization_scale: %f", element.second.as<double>());
g_value_unset(&gvalue);
}
if (element.first.find("task") != std::string::npos) {
if (element.first == "task") {
GValue gvalue = G_VALUE_INIT;
g_value_init(&gvalue, G_TYPE_STRING);
g_value_set_string(&gvalue, element.second.as<std::string>().c_str());
gst_structure_set_value(s, "anomaly_task", &gvalue);
GST_INFO("[get_model_info_postproc] anomaly_task: %s", element.second.as<std::string>().c_str());
g_value_unset(&gvalue);
}
if (element.first == "task_type") {
GValue gvalue = G_VALUE_INIT;
g_value_init(&gvalue, G_TYPE_STRING);
g_value_set_string(&gvalue, element.second.as<std::string>().c_str());
gst_structure_set_value(s, "anomaly_task_type", &gvalue);
GST_INFO("[get_model_info_postproc] anomaly_task_type: %s", element.second.as<std::string>().c_str());
g_value_unset(&gvalue);
}
if (element.first.find("labels") != std::string::npos) {
GValue gvalue = G_VALUE_INIT;
g_value_init(&gvalue, GST_TYPE_ARRAY);
Expand Down
Loading