Skip to content

Commit d5890e8

Browse files
authored
Refactor - Implement Classification (#304)
* Remove cpp implementation * Move in code from POC * Satisfy pre-commit checks * Remove precommit tests for cpp * Attempt to fix serving_api tests * Use old InputParser for test_accuracy * Remove async from public api side * Add and update license to 2025 * Remove empty line between template and class * Start implementing classification model Added multiclass classification * Implement multilabel post processing * Implement hierarchical and resolvers * Enable classification in accuracy tests * Remove rogue cout * Implement classification serialization WIP * Fix classification serialization Had to set ov::batch(model, 1); * Working serialization for instance and semantic segmentation * Attempt openvino 2025.1 for failing test * Add specific implementation for get_from_any_map for bool Sometimes the value for bool is "True" This evaluation from string is only added in openvino 2025.1.0 * Remove input shape as input for the tasks orig_width and height are stored in rt_info anyway. Serialization is a separate step and it _should_ output the same model * Satisfy clang
1 parent 25e44e7 commit d5890e8

File tree

17 files changed

+1237
-67
lines changed

17 files changed

+1237
-67
lines changed

src/cpp/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ project(vision_api
88
HOMEPAGE_URL "https://github.com/openvinotoolkit/model_api/"
99
LANGUAGES CXX C)
1010

11+
include(FetchContent)
12+
FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git
13+
GIT_TAG d41ca94fa85d5119852e2f7a3f94335cc7cb0486 # PR #4709, fixes cmake deprecation warnings
14+
)
15+
16+
FetchContent_MakeAvailable(json)
17+
18+
1119
find_package(OpenCV REQUIRED COMPONENTS core imgproc)
1220

1321
find_package(OpenVINO REQUIRED
@@ -20,5 +28,5 @@ file(GLOB ADAPTERS_SOURCES src/adapters/*.cpp)
2028

2129
add_library(model_api STATIC ${TASK_SOURCES} ${TASKS_SOURCES} ${UTILS_SOURCES} ${ADAPTERS_SOURCES} ${TILERS_SOURCES})
2230

23-
target_link_libraries(model_api PUBLIC openvino::runtime opencv_core opencv_imgproc)
31+
target_link_libraries(model_api PUBLIC openvino::runtime opencv_core opencv_imgproc PRIVATE nlohmann_json::nlohmann_json)
2432
target_include_directories(model_api PUBLIC ${PROJECT_SOURCE_DIR}/include)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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/classification/resolvers.h"
13+
#include "tasks/results.h"
14+
#include "utils/config.h"
15+
#include "utils/vision_pipeline.h"
16+
17+
class Classification {
18+
public:
19+
std::shared_ptr<InferenceAdapter> adapter;
20+
VisionPipeline<ClassificationResult> pipeline;
21+
22+
Classification(std::shared_ptr<InferenceAdapter> adapter) : adapter(adapter) {
23+
pipeline = VisionPipeline<ClassificationResult>(
24+
adapter,
25+
[&](cv::Mat image) {
26+
return preprocess(image);
27+
},
28+
[&](InferenceResult result) {
29+
return postprocess(result);
30+
});
31+
32+
auto config = adapter->getModelConfig();
33+
labels = utils::get_from_any_maps("labels", config, {}, labels);
34+
35+
topk = utils::get_from_any_maps("topk", config, {}, topk);
36+
multilabel = utils::get_from_any_maps("multilabel", config, {}, multilabel);
37+
output_raw_scores = utils::get_from_any_maps("output_raw_scores", config, {}, output_raw_scores);
38+
confidence_threshold = utils::get_from_any_maps("confidence_threshold", config, {}, confidence_threshold);
39+
hierarchical = utils::get_from_any_maps("hierarchical", config, {}, hierarchical);
40+
hierarchical_config = utils::get_from_any_maps("hierarchical_config", config, {}, hierarchical_config);
41+
hierarchical_postproc = utils::get_from_any_maps("hierarchical_postproc", config, {}, hierarchical_postproc);
42+
if (hierarchical) {
43+
if (hierarchical_config.empty()) {
44+
throw std::runtime_error("Error: empty hierarchical classification config");
45+
}
46+
hierarchical_info = HierarchicalConfig(hierarchical_config);
47+
if (hierarchical_postproc == "probabilistic") {
48+
resolver = std::make_unique<ProbabilisticLabelsResolver>(hierarchical_info);
49+
} else if (hierarchical_postproc == "greedy") {
50+
resolver = std::make_unique<GreedyLabelsResolver>(hierarchical_info);
51+
} else {
52+
throw std::runtime_error("Wrong hierarchical labels postprocessing type");
53+
}
54+
}
55+
}
56+
57+
static void serialize(std::shared_ptr<ov::Model>& ov_model);
58+
static Classification load(const std::string& model_path);
59+
60+
ClassificationResult infer(cv::Mat image);
61+
std::vector<ClassificationResult> inferBatch(std::vector<cv::Mat> image);
62+
63+
std::map<std::string, ov::Tensor> preprocess(cv::Mat);
64+
ClassificationResult postprocess(InferenceResult& infResult);
65+
66+
bool postprocess_semantic_masks = true;
67+
68+
private:
69+
ClassificationResult get_multilabel_predictions(InferenceResult& infResult, bool add_raw_scores);
70+
ClassificationResult get_multiclass_predictions(InferenceResult& infResult, bool add_raw_scores);
71+
ClassificationResult get_hierarchical_predictions(InferenceResult& infResult, bool add_raw_scores);
72+
73+
ov::Tensor reorder_saliency_maps(const ov::Tensor& source_maps);
74+
75+
// multiclass serialization step
76+
static void addOrFindSoftmaxAndTopkOutputs(std::shared_ptr<ov::Model>& model, size_t topk, bool add_raw_scores);
77+
78+
private:
79+
cv::Size input_shape;
80+
std::vector<std::string> labels;
81+
float confidence_threshold = 0.5f;
82+
83+
bool multilabel = false;
84+
bool hierarchical = false;
85+
bool output_raw_scores = false;
86+
87+
// hierarchical
88+
size_t topk = 1;
89+
std::string hierarchical_config;
90+
std::string hierarchical_postproc = "greedy";
91+
HierarchicalConfig hierarchical_info;
92+
std::unique_ptr<GreedyLabelsResolver> resolver;
93+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (C) 2020-2025 Intel Corporation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
#include <map>
6+
#include <string>
7+
#include <unordered_map>
8+
#include <vector>
9+
10+
struct HierarchicalConfig {
11+
std::map<std::string, int> label_to_idx;
12+
std::vector<std::pair<std::string, std::string>> label_tree_edges;
13+
std::vector<std::vector<std::string>> all_groups;
14+
std::map<size_t, std::pair<size_t, size_t>> head_idx_to_logits_range;
15+
std::map<size_t, std::string> logit_idx_to_label;
16+
size_t num_multiclass_heads;
17+
size_t num_multilabel_heads;
18+
size_t num_single_label_classes;
19+
20+
HierarchicalConfig() = default;
21+
HierarchicalConfig(const std::string&);
22+
};
23+
24+
class SimpleLabelsGraph {
25+
public:
26+
SimpleLabelsGraph() = default;
27+
SimpleLabelsGraph(const std::vector<std::string>& vertices_);
28+
void add_edge(const std::string& parent, const std::string& child);
29+
std::vector<std::string> get_children(const std::string& label) const;
30+
std::string get_parent(const std::string& label) const;
31+
std::vector<std::string> get_ancestors(const std::string& label) const;
32+
std::vector<std::string> get_labels_in_topological_order();
33+
34+
protected:
35+
std::vector<std::string> vertices;
36+
std::unordered_map<std::string, std::vector<std::string>> adj;
37+
std::unordered_map<std::string, std::string> parents_map;
38+
bool t_sort_cache_valid = false;
39+
std::vector<std::string> topological_order_cache;
40+
41+
std::vector<std::string> topological_sort();
42+
};
43+
44+
class GreedyLabelsResolver {
45+
public:
46+
GreedyLabelsResolver() = default;
47+
GreedyLabelsResolver(const HierarchicalConfig&);
48+
49+
virtual std::map<std::string, float> resolve_labels(const std::vector<std::reference_wrapper<std::string>>& labels,
50+
const std::vector<float>& scores);
51+
52+
protected:
53+
std::map<std::string, int> label_to_idx;
54+
std::vector<std::pair<std::string, std::string>> label_relations;
55+
std::vector<std::vector<std::string>> label_groups;
56+
57+
std::string get_parent(const std::string& label);
58+
std::vector<std::string> get_predecessors(const std::string& label, const std::vector<std::string>& candidates);
59+
};
60+
61+
class ProbabilisticLabelsResolver : public GreedyLabelsResolver {
62+
public:
63+
ProbabilisticLabelsResolver() = default;
64+
ProbabilisticLabelsResolver(const HierarchicalConfig&);
65+
66+
virtual std::map<std::string, float> resolve_labels(const std::vector<std::reference_wrapper<std::string>>& labels,
67+
const std::vector<float>& scores);
68+
std::unordered_map<std::string, float> add_missing_ancestors(const std::unordered_map<std::string, float>&) const;
69+
std::map<std::string, float> resolve_exclusive_labels(const std::unordered_map<std::string, float>&) const;
70+
void suppress_descendant_output(std::map<std::string, float>&);
71+
72+
protected:
73+
SimpleLabelsGraph label_tree;
74+
};

src/cpp/include/tasks/detection/ssd.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@ class SSD {
2828
public:
2929
std::shared_ptr<InferenceAdapter> adapter;
3030

31-
SSD(std::shared_ptr<InferenceAdapter> adapter, cv::Size input_shape) : adapter(adapter), input_shape(input_shape) {
31+
SSD(std::shared_ptr<InferenceAdapter> adapter) : adapter(adapter), input_shape(input_shape) {
3232
auto config = adapter->getModelConfig();
3333
labels = utils::get_from_any_maps("labels", config, {}, labels);
3434
confidence_threshold = utils::get_from_any_maps("confidence_threshold", config, {}, confidence_threshold);
35+
input_shape.width = utils::get_from_any_maps("orig_width", config, {}, input_shape.width);
36+
input_shape.height = utils::get_from_any_maps("orig_height", config, {}, input_shape.height);
3537
}
3638
std::map<std::string, ov::Tensor> preprocess(cv::Mat);
3739
DetectionResult postprocess(InferenceResult& infResult);
3840

39-
static cv::Size serialize(std::shared_ptr<ov::Model> ov_model);
41+
static void serialize(std::shared_ptr<ov::Model> ov_model);
4042

4143
SSDOutputMode output_mode;
4244

src/cpp/include/tasks/instance_segmentation.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ class InstanceSegmentation {
1818
std::shared_ptr<InferenceAdapter> adapter;
1919
VisionPipeline<InstanceSegmentationResult> pipeline;
2020

21-
InstanceSegmentation(std::shared_ptr<InferenceAdapter> adapter, cv::Size input_shape)
22-
: adapter(adapter),
23-
input_shape(input_shape) {
21+
InstanceSegmentation(std::shared_ptr<InferenceAdapter> adapter) : adapter(adapter) {
2422
pipeline = VisionPipeline<InstanceSegmentationResult>(
2523
adapter,
2624
[&](cv::Mat image) {
@@ -33,9 +31,11 @@ class InstanceSegmentation {
3331
auto config = adapter->getModelConfig();
3432
labels = utils::get_from_any_maps("labels", config, {}, labels);
3533
confidence_threshold = utils::get_from_any_maps("confidence_threshold", config, {}, confidence_threshold);
34+
input_shape.width = utils::get_from_any_maps("orig_width", config, {}, input_shape.width);
35+
input_shape.height = utils::get_from_any_maps("orig_height", config, {}, input_shape.width);
3636
}
3737

38-
static cv::Size serialize(std::shared_ptr<ov::Model>& ov_model);
38+
static void serialize(std::shared_ptr<ov::Model>& ov_model);
3939
static InstanceSegmentation load(const std::string& model_path);
4040

4141
InstanceSegmentationResult infer(cv::Mat image);

src/cpp/include/tasks/results.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,50 @@ struct InstanceSegmentationResult {
146146
std::vector<cv::Mat_<std::uint8_t>> saliency_map;
147147
ov::Tensor feature_vector;
148148
};
149+
150+
struct ClassificationResult {
151+
friend std::ostream& operator<<(std::ostream& os, const ClassificationResult& prediction) {
152+
for (const ClassificationResult::Classification& classification : prediction.topLabels) {
153+
os << classification << ", ";
154+
}
155+
try {
156+
os << prediction.saliency_map.get_shape() << ", ";
157+
} catch (ov::Exception&) {
158+
os << "[0], ";
159+
}
160+
try {
161+
os << prediction.feature_vector.get_shape() << ", ";
162+
} catch (ov::Exception&) {
163+
os << "[0], ";
164+
}
165+
try {
166+
os << prediction.raw_scores.get_shape();
167+
} catch (ov::Exception&) {
168+
os << "[0]";
169+
}
170+
return os;
171+
}
172+
173+
explicit operator std::string() {
174+
std::stringstream ss;
175+
ss << *this;
176+
return ss.str();
177+
}
178+
179+
struct Classification {
180+
size_t id;
181+
std::string label;
182+
float score;
183+
184+
Classification(size_t id, const std::string& label, float score) : id(id), label(label), score(score) {}
185+
186+
friend std::ostream& operator<<(std::ostream& os, const Classification& prediction) {
187+
return os << prediction.id << " (" << prediction.label << "): " << std::fixed << std::setprecision(3)
188+
<< prediction.score;
189+
}
190+
};
191+
192+
std::vector<Classification> topLabels;
193+
ov::Tensor saliency_map, feature_vector,
194+
raw_scores; // Contains "raw_scores", "saliency_map" and "feature_vector" model outputs if such exist
195+
};

src/cpp/include/tasks/semantic_segmentation.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class SemanticSegmentation {
3333
blur_strength = utils::get_from_any_maps("blur_strength", config, {}, blur_strength);
3434
}
3535

36-
static cv::Size serialize(std::shared_ptr<ov::Model>& ov_model);
36+
static void serialize(std::shared_ptr<ov::Model>& ov_model);
3737
static SemanticSegmentation load(const std::string& model_path);
3838

3939
std::map<std::string, ov::Tensor> preprocess(cv::Mat);

src/cpp/include/utils/config.h

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ Type get_from_any_maps(const std::string& key,
2424
return low_priority;
2525
}
2626

27+
template <>
28+
inline bool get_from_any_maps(const std::string& key,
29+
const ov::AnyMap& top_priority,
30+
const ov::AnyMap& mid_priority,
31+
bool low_priority) {
32+
auto topk_iter = top_priority.find(key);
33+
if (topk_iter != top_priority.end()) {
34+
const std::string& val = topk_iter->second.as<std::string>();
35+
return val == "True" || val == "YES";
36+
}
37+
topk_iter = mid_priority.find(key);
38+
if (topk_iter != mid_priority.end()) {
39+
const std::string& val = topk_iter->second.as<std::string>();
40+
return val == "True" || val == "YES";
41+
}
42+
return low_priority;
43+
}
44+
2745
inline bool model_has_embedded_processing(std::shared_ptr<ov::Model> model) {
2846
if (model->has_rt_info("model_info")) {
2947
auto model_info = model->get_rt_info<ov::AnyMap>("model_info");
@@ -97,6 +115,31 @@ static inline std::tuple<bool, ov::Layout> makeGuesLayoutFrom4DShape(const ov::P
97115
return {false, ov::Layout{}};
98116
}
99117

118+
static inline std::map<std::string, ov::Layout> parseLayoutString(const std::string& layout_string) {
119+
// Parse parameter string like "input0:NCHW,input1:NC" or "NCHW" (applied to all
120+
// inputs)
121+
std::map<std::string, ov::Layout> layouts;
122+
std::string searchStr =
123+
(layout_string.find_last_of(':') == std::string::npos && !layout_string.empty() ? ":" : "") + layout_string;
124+
auto colonPos = searchStr.find_last_of(':');
125+
while (colonPos != std::string::npos) {
126+
auto startPos = searchStr.find_last_of(',');
127+
auto inputName = searchStr.substr(startPos + 1, colonPos - startPos - 1);
128+
auto inputLayout = searchStr.substr(colonPos + 1);
129+
layouts[inputName] = ov::Layout(inputLayout);
130+
searchStr.resize(startPos + 1);
131+
if (searchStr.empty() || searchStr.back() != ',') {
132+
break;
133+
}
134+
searchStr.pop_back();
135+
colonPos = searchStr.find_last_of(':');
136+
}
137+
if (!searchStr.empty()) {
138+
throw std::invalid_argument("Can't parse input layout string: " + layout_string);
139+
}
140+
return layouts;
141+
}
142+
100143
static inline ov::Layout getLayoutFromShape(const ov::PartialShape& shape) {
101144
if (shape.size() == 2) {
102145
return "NC";
@@ -133,4 +176,22 @@ static inline ov::Layout getLayoutFromShape(const ov::PartialShape& shape) {
133176
throw std::runtime_error("Usupported " + std::to_string(shape.size()) + "D shape");
134177
}
135178

179+
static inline ov::Layout getInputLayout(const ov::Output<ov::Node>& input,
180+
std::map<std::string, ov::Layout>& inputsLayouts) {
181+
ov::Layout layout = ov::layout::get_layout(input);
182+
if (layout.empty()) {
183+
if (inputsLayouts.empty()) {
184+
layout = getLayoutFromShape(input.get_partial_shape());
185+
std::cout << "Automatically detected layout '" << layout.to_string() << "' for input '"
186+
<< input.get_any_name() << "' will be used." << std::endl;
187+
} else if (inputsLayouts.size() == 1) {
188+
layout = inputsLayouts.begin()->second;
189+
} else {
190+
layout = inputsLayouts[input.get_any_name()];
191+
}
192+
}
193+
194+
return layout;
195+
}
196+
136197
} // namespace utils

0 commit comments

Comments
 (0)