Skip to content

Commit 8674640

Browse files
committed
Merge branch 'feature/cpp_refactoring' into rhecker/semantic_segmentation_tiling
2 parents 4e5a9df + 606fa98 commit 8674640

File tree

5 files changed

+308
-0
lines changed

5 files changed

+308
-0
lines changed

src/cpp/include/tasks/anomaly.h

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2020-2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#pragma once
7+
8+
#include <opencv2/opencv.hpp>
9+
#include <openvino/openvino.hpp>
10+
11+
#include "adapters/inference_adapter.h"
12+
#include "tasks/results.h"
13+
#include "utils/config.h"
14+
#include "utils/vision_pipeline.h"
15+
16+
class Anomaly {
17+
public:
18+
std::shared_ptr<InferenceAdapter> adapter;
19+
VisionPipeline<AnomalyResult> pipeline;
20+
21+
Anomaly(std::shared_ptr<InferenceAdapter> adapter) : adapter(adapter) {
22+
pipeline = VisionPipeline<AnomalyResult>(
23+
adapter,
24+
[&](cv::Mat image) {
25+
return preprocess(image);
26+
},
27+
[&](InferenceResult result) {
28+
return postprocess(result);
29+
});
30+
31+
auto config = adapter->getModelConfig();
32+
image_threshold = utils::get_from_any_maps("image_threshold", config, {}, image_threshold);
33+
pixel_threshold = utils::get_from_any_maps("pixel_threshold", config, {}, pixel_threshold);
34+
normalization_scale = utils::get_from_any_maps("normalization_scale", config, {}, normalization_scale);
35+
task = utils::get_from_any_maps("pixel_threshold", config, {}, task);
36+
labels = utils::get_from_any_maps("labels", config, {}, labels);
37+
input_shape.width = utils::get_from_any_maps("orig_width", config, {}, input_shape.width);
38+
input_shape.height = utils::get_from_any_maps("orig_height", config, {}, input_shape.height);
39+
}
40+
41+
static void serialize(std::shared_ptr<ov::Model>& ov_model);
42+
static Anomaly load(const std::string& model_path);
43+
44+
AnomalyResult infer(cv::Mat image);
45+
std::vector<AnomalyResult> inferBatch(std::vector<cv::Mat> image);
46+
47+
std::map<std::string, ov::Tensor> preprocess(cv::Mat);
48+
AnomalyResult postprocess(InferenceResult& infResult);
49+
50+
private:
51+
cv::Mat normalize(cv::Mat& tensor, float threshold);
52+
double normalize(double& tensor, float threshold);
53+
std::vector<cv::Rect> getBoxes(cv::Mat& mask);
54+
55+
private:
56+
cv::Size input_shape;
57+
std::vector<std::string> labels;
58+
59+
float image_threshold = 0.5f;
60+
float pixel_threshold = 0.5f;
61+
float normalization_scale = 1.0f;
62+
std::string task = "segmentation";
63+
};

src/cpp/include/tasks/results.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,37 @@ struct ClassificationResult {
193193
ov::Tensor saliency_map, feature_vector,
194194
raw_scores; // Contains "raw_scores", "saliency_map" and "feature_vector" model outputs if such exist
195195
};
196+
197+
struct AnomalyResult {
198+
cv::Mat anomaly_map;
199+
std::vector<cv::Rect> pred_boxes;
200+
std::string pred_label;
201+
cv::Mat pred_mask;
202+
double pred_score;
203+
204+
friend std::ostream& operator<<(std::ostream& os, const AnomalyResult& prediction) {
205+
double min_anomaly_map, max_anomaly_map;
206+
cv::minMaxLoc(prediction.anomaly_map, &min_anomaly_map, &max_anomaly_map);
207+
double min_pred_mask, max_pred_mask;
208+
cv::minMaxLoc(prediction.pred_mask, &min_pred_mask, &max_pred_mask);
209+
os << "anomaly_map min:" << min_anomaly_map << " max:" << max_anomaly_map << ";";
210+
os << "pred_score:" << std::fixed << std::setprecision(1) << prediction.pred_score << ";";
211+
os << "pred_label:" << prediction.pred_label << ";";
212+
os << std::fixed << std::setprecision(0) << "pred_mask min:" << min_pred_mask << " max:" << max_pred_mask
213+
<< ";";
214+
215+
if (!prediction.pred_boxes.empty()) {
216+
os << "pred_boxes:";
217+
for (const cv::Rect& box : prediction.pred_boxes) {
218+
os << box << ",";
219+
}
220+
}
221+
222+
return os;
223+
}
224+
explicit operator std::string() {
225+
std::stringstream ss;
226+
ss << *this;
227+
return ss.str();
228+
}
229+
};

src/cpp/src/tasks/anomaly.cpp

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#include "tasks/anomaly.h"
2+
3+
#include "adapters/openvino_adapter.h"
4+
#include "utils/preprocessing.h"
5+
#include "utils/tensor.h"
6+
7+
void Anomaly::serialize(std::shared_ptr<ov::Model>& ov_model) {
8+
auto input = ov_model->inputs().front();
9+
10+
auto layout = ov::layout::get_layout(input);
11+
if (layout.empty()) {
12+
layout = utils::getLayoutFromShape(input.get_partial_shape());
13+
}
14+
15+
const ov::Shape& shape = input.get_partial_shape().get_max_shape();
16+
17+
auto interpolation_mode = cv::INTER_LINEAR;
18+
utils::RESIZE_MODE resize_mode = utils::RESIZE_FILL;
19+
uint8_t pad_value = 0;
20+
bool reverse_input_channels = false;
21+
22+
std::vector<float> scale_values;
23+
std::vector<float> mean_values;
24+
if (ov_model->has_rt_info("model_info")) {
25+
auto config = ov_model->get_rt_info<ov::AnyMap>("model_info");
26+
reverse_input_channels =
27+
utils::get_from_any_maps("reverse_input_channels", config, ov::AnyMap{}, reverse_input_channels);
28+
scale_values = utils::get_from_any_maps("scale_values", config, ov::AnyMap{}, scale_values);
29+
mean_values = utils::get_from_any_maps("mean_values", config, ov::AnyMap{}, mean_values);
30+
}
31+
32+
auto input_shape = ov::Shape{shape[ov::layout::width_idx(layout)], shape[ov::layout::height_idx(layout)]};
33+
34+
ov_model = utils::embedProcessing(ov_model,
35+
input.get_any_name(),
36+
layout,
37+
resize_mode,
38+
interpolation_mode,
39+
input_shape,
40+
pad_value,
41+
reverse_input_channels,
42+
mean_values,
43+
scale_values);
44+
45+
ov_model->set_rt_info(input_shape[0], "model_info", "orig_width");
46+
ov_model->set_rt_info(input_shape[1], "model_info", "orig_height");
47+
}
48+
49+
Anomaly Anomaly::load(const std::string& model_path) {
50+
auto core = ov::Core();
51+
std::shared_ptr<ov::Model> model = core.read_model(model_path);
52+
53+
if (model->has_rt_info("model_info", "model_type")) {
54+
std::cout << "has model type in info: " << model->get_rt_info<std::string>("model_info", "model_type")
55+
<< std::endl;
56+
} else {
57+
throw std::runtime_error("Incorrect or unsupported model_type");
58+
}
59+
60+
if (utils::model_has_embedded_processing(model)) {
61+
std::cout << "model already was serialized" << std::endl;
62+
} else {
63+
serialize(model);
64+
}
65+
auto adapter = std::make_shared<OpenVINOInferenceAdapter>();
66+
adapter->loadModel(model, core, "AUTO");
67+
return Anomaly(adapter);
68+
}
69+
70+
AnomalyResult Anomaly::infer(cv::Mat image) {
71+
return pipeline.infer(image);
72+
}
73+
74+
std::vector<AnomalyResult> Anomaly::inferBatch(std::vector<cv::Mat> images) {
75+
return pipeline.inferBatch(images);
76+
}
77+
78+
std::map<std::string, ov::Tensor> Anomaly::preprocess(cv::Mat image) {
79+
std::map<std::string, ov::Tensor> input = {};
80+
input.emplace(adapter->getInputNames()[0], utils::wrapMat2Tensor(image));
81+
return input;
82+
}
83+
84+
AnomalyResult Anomaly::postprocess(InferenceResult& infResult) {
85+
auto tensorName = adapter->getOutputNames().front();
86+
ov::Tensor predictions = infResult.data[tensorName];
87+
const auto& inputImgSize = infResult.inputImageSize;
88+
89+
double pred_score;
90+
std::string pred_label;
91+
cv::Mat anomaly_map;
92+
cv::Mat pred_mask;
93+
std::vector<cv::Rect> pred_boxes;
94+
if (predictions.get_shape().size() == 1) {
95+
pred_score = predictions.data<float>()[0];
96+
} else {
97+
const ov::Layout& layout = utils::getLayoutFromShape(predictions.get_shape());
98+
const ov::Shape& predictionsShape = predictions.get_shape();
99+
anomaly_map = cv::Mat(static_cast<int>(predictionsShape[ov::layout::height_idx(layout)]),
100+
static_cast<int>(predictionsShape[ov::layout::width_idx(layout)]),
101+
CV_32FC1,
102+
predictions.data<float>());
103+
// find the max predicted score
104+
cv::minMaxLoc(anomaly_map, NULL, &pred_score);
105+
}
106+
pred_label = labels[pred_score > image_threshold ? 1 : 0];
107+
108+
pred_mask = anomaly_map >= pixel_threshold;
109+
pred_mask.convertTo(pred_mask, CV_8UC1, 1 / 255.);
110+
cv::resize(pred_mask, pred_mask, cv::Size{inputImgSize.width, inputImgSize.height});
111+
anomaly_map = normalize(anomaly_map, pixel_threshold);
112+
anomaly_map.convertTo(anomaly_map, CV_8UC1, 255);
113+
114+
pred_score = normalize(pred_score, image_threshold);
115+
if (pred_label == labels[0]) { // normal label
116+
pred_score = 1 - pred_score; // Score of normal is 1 - score of anomaly
117+
}
118+
119+
if (!anomaly_map.empty()) {
120+
cv::resize(anomaly_map, anomaly_map, cv::Size{inputImgSize.width, inputImgSize.height});
121+
}
122+
123+
if (task == "detection") {
124+
pred_boxes = getBoxes(pred_mask);
125+
}
126+
127+
AnomalyResult result;
128+
result.anomaly_map = std::move(anomaly_map);
129+
result.pred_score = pred_score;
130+
result.pred_label = std::move(pred_label);
131+
result.pred_mask = std::move(pred_mask);
132+
result.pred_boxes = std::move(pred_boxes);
133+
return result;
134+
}
135+
136+
cv::Mat Anomaly::normalize(cv::Mat& tensor, float threshold) {
137+
cv::Mat normalized = ((tensor - threshold) / normalization_scale) + 0.5f;
138+
normalized = cv::min(cv::max(normalized, 0.f), 1.f);
139+
return normalized;
140+
}
141+
142+
double Anomaly::normalize(double& value, float threshold) {
143+
double normalized = ((value - threshold) / normalization_scale) + 0.5f;
144+
return std::min(std::max(normalized, 0.), 1.);
145+
}
146+
147+
std::vector<cv::Rect> Anomaly::getBoxes(cv::Mat& mask) {
148+
std::vector<cv::Rect> boxes;
149+
std::vector<std::vector<cv::Point>> contours;
150+
cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
151+
for (auto& contour : contours) {
152+
std::vector<int> box;
153+
cv::Rect rect = cv::boundingRect(contour);
154+
boxes.push_back(rect);
155+
}
156+
return boxes;
157+
}

tests/cpp/test_accuracy.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <thread>
88

99
#include "matchers.h"
10+
#include "tasks/anomaly.h"
1011
#include "tasks/classification.h"
1112
#include "tasks/detection.h"
1213
#include "tasks/instance_segmentation.h"
@@ -126,6 +127,16 @@ TEST_P(ModelParameterizedTest, AccuracyTest) {
126127
std::string image_path = DATA_DIR + '/' + test_data.image;
127128
auto image = load_image(image_path, use_tiling, data.input_res);
128129
auto result = model.infer(image);
130+
EXPECT_EQ(std::string{result}, test_data.reference[0]);
131+
}
132+
} else if (data.type == "AnomalyDetection") {
133+
auto model = Anomaly::load(model_path);
134+
135+
for (auto& test_data : data.test_data) {
136+
std::string image_path = DATA_DIR + '/' + test_data.image;
137+
cv::Mat image = cv::imread(image_path);
138+
auto result = model.infer(image);
139+
129140
EXPECT_EQ(std::string{result}, test_data.reference[0]);
130141
}
131142
} else {
@@ -169,7 +180,15 @@ TEST_P(ModelParameterizedTest, SerializedAccuracyTest) {
169180
}
170181
} else if (data.type == "ClassificationModel") {
171182
auto model = Classification::load(model_path);
183+
for (auto& test_data : data.test_data) {
184+
std::string image_path = DATA_DIR + '/' + test_data.image;
185+
cv::Mat image = cv::imread(image_path);
186+
auto result = model.infer(image);
172187

188+
EXPECT_EQ(std::string{result}, test_data.reference[0]);
189+
}
190+
} else if (data.type == "AnomalyDetection") {
191+
auto model = Anomaly::load(model_path);
173192
for (auto& test_data : data.test_data) {
174193
std::string image_path = DATA_DIR + '/' + test_data.image;
175194
auto image = load_image(image_path, use_tiling, data.input_res);
@@ -228,6 +247,17 @@ TEST_P(ModelParameterizedTest, AccuracyTestBatch) {
228247
auto image = load_image(image_path, use_tiling, data.input_res);
229248
auto result = model.inferBatch({image});
230249

250+
ASSERT_EQ(result.size(), 1);
251+
EXPECT_EQ(std::string{result[0]}, test_data.reference[0]);
252+
}
253+
} else if (data.type == "AnomalyDetection") {
254+
auto model = Anomaly::load(model_path);
255+
256+
for (auto& test_data : data.test_data) {
257+
std::string image_path = DATA_DIR + '/' + test_data.image;
258+
cv::Mat image = cv::imread(image_path);
259+
auto result = model.inferBatch({image});
260+
231261
ASSERT_EQ(result.size(), 1);
232262
EXPECT_EQ(std::string{result[0]}, test_data.reference[0]);
233263
}

tests/python/accuracy/public_scope.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,5 +187,29 @@
187187
"reference": ["0 (1): 0.849, [0], [0], [0]"]
188188
}
189189
]
190+
},
191+
{
192+
"name": "otx_models/anomaly_padim_bottle_mvtec.xml",
193+
"type": "AnomalyDetection",
194+
"test_data": [
195+
{
196+
"image": "coco128/images/train2017/000000000074.jpg",
197+
"reference": [
198+
"anomaly_map min:151 max:255;pred_score:1.0;pred_label:Anomaly;pred_mask min:1 max:1;"
199+
]
200+
}
201+
]
202+
},
203+
{
204+
"name": "otx_models/anomaly_stfpm_bottle_mvtec.xml",
205+
"type": "AnomalyDetection",
206+
"test_data": [
207+
{
208+
"image": "coco128/images/train2017/000000000074.jpg",
209+
"reference": [
210+
"anomaly_map min:124 max:225;pred_score:0.9;pred_label:Anomaly;pred_mask min:0 max:1;"
211+
]
212+
}
213+
]
190214
}
191215
]

0 commit comments

Comments
 (0)