diff --git a/docs/template_plugin/tests/functional/subgraph_reference/preprocess.cpp b/docs/template_plugin/tests/functional/subgraph_reference/preprocess.cpp index 5b00255b2a7..d2f32866671 100644 --- a/docs/template_plugin/tests/functional/subgraph_reference/preprocess.cpp +++ b/docs/template_plugin/tests/functional/subgraph_reference/preprocess.cpp @@ -766,6 +766,36 @@ static RefPreprocessParams convert_color_i420_single_plane() { return res; } +static RefPreprocessParams set_shape_custom_crop() { + RefPreprocessParams res("set_shape_custom_crop"); + res.function = []() { + auto f = create_simple_function(element::f32, PartialShape{2, 2, 2, 2}); + auto p = PrePostProcessor(f); + p.input().tensor().set_shape({-1, -1, -1, -1}); + p.input().preprocess().custom([](const Output& node) { + // Add custom crop to model's dimensions using 'Slice' operation + // Middle part 2x2x2x2 of original user's 4x4x4x4 input tensor will be extracted + auto start = opset8::Constant::create(element::i32, {4}, {1, 1, 1, 1}); + auto stop = opset8::Constant::create(element::i32, {4}, {3, 3, 3, 3}); + auto step = opset8::Constant::create(element::i32, {4}, {1, 1, 1, 1}); + auto axis = opset8::Constant::create(element::i32, {4}, {0, 1, 2, 3}); + auto slice = std::make_shared(node, start, stop, step, axis); + return slice; + }); + p.build(); + return f; + }; + auto input_size = 4 * 4 * 4 * 4; + std::vector input_values(input_size); + std::iota(input_values.begin(), input_values.end(), 0); + res.inputs.emplace_back(element::f32, Shape{4, 4, 4, 4}, input_values); + res.expected.emplace_back(Shape{2, 2, 2, 2}, element::f32, std::vector{ 85, 86, 89, 90, + 101, 102, 105, 106, + 149, 150, 153, 154, + 165, 166, 169, 170}); + return res; +} + static RefPreprocessParams postprocess_2_inputs_basic() { RefPreprocessParams res("postprocess_2_inputs_basic"); res.function = []() { @@ -1037,6 +1067,7 @@ std::vector allPreprocessTests() { element_type_before_convert_color_nv12(), convert_color_i420_to_bgr_three_planes(), convert_color_i420_single_plane(), + set_shape_custom_crop(), postprocess_2_inputs_basic(), post_convert_layout_by_dims(), post_convert_layout_by_dims_multi(), diff --git a/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp b/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp index 1393ff7494c..c7965d9d95f 100644 --- a/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp +++ b/src/bindings/python/src/pyopenvino/graph/preprocess/pre_post_process.cpp @@ -255,7 +255,13 @@ static void regclass_graph_InputTensorInfo(py::module m) { }); info.def("set_spatial_static_shape", [](ov::preprocess::InputTensorInfo& me, size_t height, size_t width) { return &me.set_spatial_static_shape(height, width); - ; + }); + info.def("set_shape", [](ov::preprocess::InputTensorInfo& me, const ov::PartialShape& shape) { + return &me.set_shape(shape); + }); + // Allow to use set_shape([1,2,3]) in Python code, not set_shape(PartialShape([1,2,3])) + info.def("set_shape", [](ov::preprocess::InputTensorInfo& me, const std::vector& shape) { + return &me.set_shape(shape); }); info.def("set_color_format", [](ov::preprocess::InputTensorInfo& me, diff --git a/src/bindings/python/tests/test_ngraph/test_preprocess.py b/src/bindings/python/tests/test_ngraph/test_preprocess.py index 5e1c0ae8bc6..1199e01f5a1 100644 --- a/src/bindings/python/tests/test_ngraph/test_preprocess.py +++ b/src/bindings/python/tests/test_ngraph/test_preprocess.py @@ -201,6 +201,37 @@ def test_ngraph_preprocess_spatial_static_shape(): assert np.equal(output, expected_output).all() +def test_ngraph_preprocess_set_shape(): + shape = [1, 1, 1] + parameter_a = ops.parameter(shape, dtype=np.int32, name="A") + model = parameter_a + function = Function(model, [parameter_a], "TestFunction") + + @custom_preprocess_function + def custom_crop(out_node: Output): + start = ops.constant(np.array([1, 1, 1]), dtype=np.int32) + stop = ops.constant(np.array([2, 2, 2]), dtype=np.int32) + step = ops.constant(np.array([1, 1, 1]), dtype=np.int32) + axis = ops.constant(np.array([0, 1, 2]), dtype=np.int32) + return ops.slice(out_node, start, stop, step, axis) + + p = PrePostProcessor(function) + inp = p.input() + inp.tensor().set_shape([3, 3, 3]) + inp.preprocess().custom(custom_crop) + function = p.build() + + input_data = np.array([[[0, 1, 2], [3, 4, 5], [6, 7, 8]], + [[9, 10, 11], [12, 13, 14], [15, 16, 17]], + [[18, 19, 20], [21, 22, 23], [24, 25, 26]]]).astype(np.int32) + expected_output = np.array([[[13]]]).astype(np.float32) + + runtime = get_runtime() + computation = runtime.computation(function) + output = computation(input_data) + assert np.equal(output, expected_output).all() + + @pytest.mark.parametrize( "algorithm, color_format1, color_format2, is_failing", [(ResizeAlgorithm.RESIZE_LINEAR, ColorFormat.UNDEFINED, ColorFormat.BGR, True), diff --git a/src/core/include/openvino/core/preprocess/input_tensor_info.hpp b/src/core/include/openvino/core/preprocess/input_tensor_info.hpp index fc7c25e701a..f554c485b3c 100644 --- a/src/core/include/openvino/core/preprocess/input_tensor_info.hpp +++ b/src/core/include/openvino/core/preprocess/input_tensor_info.hpp @@ -105,6 +105,19 @@ public: /// /// \return Reference to 'this' to allow chaining with other calls in a builder-like manner InputTensorInfo& set_memory_type(const std::string& memory_type); + + /// \brief By default, input shape is inherited from model's input shape. Use this method to specify different + /// input data shape. If it is needed to change only input height & width of input image, consider define layout and + /// use `set_spatial_static_shape' or 'set_spatial_dynamic_shape' instead. This method allows defining any custom + /// input shape and can be useful for custom preprocessing operations + /// + /// \note Methods 'set_spatial_dynamic_shape', 'set_spatial_static_shape' are also intended to modify input shape, + /// using those methods together will throw ov::AssertFailure exception + /// + /// \param shape New shape for input tensor. + /// + /// \return Reference to 'this' to allow chaining with other calls in a builder-like manner. + InputTensorInfo& set_shape(const ov::PartialShape& shape); }; } // namespace preprocess diff --git a/src/core/src/preprocess/pre_post_process.cpp b/src/core/src/preprocess/pre_post_process.cpp index eb9134477be..eb54293061d 100644 --- a/src/core/src/preprocess/pre_post_process.cpp +++ b/src/core/src/preprocess/pre_post_process.cpp @@ -69,12 +69,14 @@ public: } void set_spatial_dynamic_shape() { + OPENVINO_ASSERT(!m_shape_set, "'set_spatial_dynamic_shape' and 'set_shape' shall not be used together"); m_spatial_shape_set = true; m_spatial_width = -1; m_spatial_height = -1; } void set_spatial_static_shape(size_t height, size_t width) & { + OPENVINO_ASSERT(!m_shape_set, "'set_spatial_static_shape' and 'set_shape' shall not be used together"); m_spatial_shape_set = true; m_spatial_height = static_cast(height); m_spatial_width = static_cast(width); @@ -122,6 +124,22 @@ public: return m_memory_type_set; } + void set_shape(const PartialShape& shape) { + OPENVINO_ASSERT( + !m_spatial_shape_set, + "'set_spatial_static_shape', 'set_spatial_dynamic_shape', 'set_shape' shall not be used together"); + m_shape = shape; + m_shape_set = true; + } + + bool is_shape_set() const { + return m_shape_set; + } + + const PartialShape& get_shape() const { + return m_shape; + } + private: ColorFormat m_color_format = ColorFormat::UNDEFINED; std::vector m_planes_sub_names; @@ -138,6 +156,9 @@ private: std::string m_memory_type = {}; bool m_memory_type_set = false; + + PartialShape m_shape = {}; + bool m_shape_set = false; }; class OutputTensorInfo::OutputTensorInfoImpl : public TensorInfoImplBase {}; @@ -371,6 +392,9 @@ std::shared_ptr PrePostProcessor::build() { auto net_shape = param->get_partial_shape(); auto new_param_shape = net_shape; + if (input->get_tensor_data()->is_shape_set()) { + new_param_shape = input->get_tensor_data()->get_shape(); + } if (input->get_tensor_data()->is_layout_set() && !param->get_layout().empty() && param->get_layout() != input->get_tensor_data()->get_layout()) { // Find transpose between network and tensor layouts and update tensor shape @@ -652,6 +676,11 @@ InputTensorInfo& InputTensorInfo::set_memory_type(const std::string& memory_type return *this; } +InputTensorInfo& InputTensorInfo::set_shape(const PartialShape& shape) { + m_impl->set_shape(shape); + return *this; +} + // --------------------- PreProcessSteps ------------------ PreProcessSteps::PreProcessSteps() : m_impl(std::unique_ptr(new PreProcessStepsImpl())) {} diff --git a/src/core/tests/preprocess.cpp b/src/core/tests/preprocess.cpp index a6a15f7418e..30dcb3bea87 100644 --- a/src/core/tests/preprocess.cpp +++ b/src/core/tests/preprocess.cpp @@ -636,6 +636,42 @@ TEST(pre_post_process, tensor_spatial_shape_no_layout_dims) { p.build(), ov::AssertFailure); } +TEST(pre_post_process, tensor_set_shape_incompatible) { + auto f = create_simple_function(element::f32, Shape{1, 3, 224, 224}); + auto p = PrePostProcessor(f); + EXPECT_THROW(p.input().tensor().set_shape({1, 4, 224, 224}); p.build(), ov::AssertFailure); +} + +// Check that 'set_shape' shall not be used together with set_spatial_..._shape +// This test can be removed if this requirement is relaxed in future releases +TEST(pre_post_process, tensor_set_shape_with_spatial) { + auto f = create_simple_function(element::f32, PartialShape{-1, -1, -1, -1}); + { + auto p = PrePostProcessor(f); + p.input().tensor().set_layout("NCHW"); + EXPECT_THROW(p.input().tensor().set_shape({1, 3, 224, 224}).set_spatial_static_shape(448, 448); + p.build(), ov::AssertFailure); + } + { + auto p = PrePostProcessor(f); + p.input().tensor().set_layout("NCHW"); + EXPECT_THROW(p.input().tensor().set_spatial_static_shape(448, 448).set_shape({1, 3, 224, 224}); + p.build(), ov::AssertFailure); + } + { + auto p = PrePostProcessor(f); + p.input().tensor().set_layout("NCHW"); + EXPECT_THROW(p.input().tensor().set_shape({1, 3, 224, 224}).set_spatial_dynamic_shape(); + p.build(), ov::AssertFailure); + } + { + auto p = PrePostProcessor(f); + p.input().tensor().set_layout("NCHW"); + EXPECT_THROW(p.input().tensor().set_spatial_dynamic_shape().set_shape({1, 3, 224, 224}); + p.build(), ov::AssertFailure); + } +} + TEST(pre_post_process, resize_no_tensor_height) { auto f = create_simple_function(element::f32, Shape{1, 3, 224, 224}); auto p = PrePostProcessor(f);