Repository: openvinotoolkit/datumaro

Issue:
explain command cannot find the model 
When I try to infer an image with a model included in the project using the command 'explain', I get the error : 
**AttributeError: 'Tree' object has no attribute 'models'**

However, when I check the project info I see that the model is successfully added to the project, and the model can also produce annotations with 'model run' command 

Any help will be appreciated.
![Screenshot from 2022-07-28 11-34-46](https://user-images.githubusercontent.com/110161019/181460641-5c25f77e-f7fb-4347-9ab3-efc14f912b03.png)
 


Discussion:

Resolving patch:
diff --git a/datumaro/cli/commands/explain.py b/datumaro/cli/commands/explain.py
--- a/datumaro/cli/commands/explain.py
+++ b/datumaro/cli/commands/explain.py
@@ -188,7 +188,13 @@ def explain_command(args):
 
     project = scope_add(load_project(args.project_dir))
 
-    model = project.working_tree.models.make_executable_model(args.model)
+    if args.model not in project.models:
+        raise CliException(
+            f"{args.model} has not been registered in this project. "
+            f"This is a list of registered model names: {list(project.models.keys())}"
+        )
+
+    model = project.make_model(args.model)
 
     if str(args.algorithm).lower() != "rise":
         raise CliException("Explanation algorithm except for rise is not yet implemented.")


Test patch:
diff --git a/tests/assets/rise/model.bin b/tests/assets/rise/model.bin
new file mode 100644
Binary files /dev/null and b/tests/assets/rise/model.bin differ
diff --git a/tests/assets/rise/model.xml b/tests/assets/rise/model.xml
new file mode 100644
--- /dev/null
+++ b/tests/assets/rise/model.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+<net name="torch-jit-export" version="11">
+    <layers>
+        <layer id="0" name="input" type="Parameter" version="opset1">
+            <data shape="1,3,224,224" element_type="f32" />
+            <output>
+                <port id="0" precision="FP32" names="input">
+                    <dim>1</dim>
+                    <dim>3</dim>
+                    <dim>224</dim>
+                    <dim>224</dim>
+                </port>
+            </output>
+        </layer>
+        <layer id="1" name="Range_7" type="Const" version="opset1">
+            <data element_type="i64" shape="2" offset="0" size="16" />
+            <output>
+                <port id="0" precision="I64">
+                    <dim>2</dim>
+                </port>
+            </output>
+        </layer>
+        <layer id="2" name="GlobalAveragePool_0" type="ReduceMean" version="opset1">
+            <data keep_dims="true" />
+            <input>
+                <port id="0" precision="FP32">
+                    <dim>1</dim>
+                    <dim>3</dim>
+                    <dim>224</dim>
+                    <dim>224</dim>
+                </port>
+                <port id="1" precision="I64">
+                    <dim>2</dim>
+                </port>
+            </input>
+            <output>
+                <port id="2" precision="FP32" names="1">
+                    <dim>1</dim>
+                    <dim>3</dim>
+                    <dim>1</dim>
+                    <dim>1</dim>
+                </port>
+            </output>
+        </layer>
+        <layer id="3" name="Constant_32" type="Const" version="opset1">
+            <data element_type="i64" shape="2" offset="16" size="16" />
+            <output>
+                <port id="0" precision="I64">
+                    <dim>2</dim>
+                </port>
+            </output>
+        </layer>
+        <layer id="4" name="2" type="Reshape" version="opset1">
+            <data special_zero="true" />
+            <input>
+                <port id="0" precision="FP32">
+                    <dim>1</dim>
+                    <dim>3</dim>
+                    <dim>1</dim>
+                    <dim>1</dim>
+                </port>
+                <port id="1" precision="I64">
+                    <dim>2</dim>
+                </port>
+            </input>
+            <output>
+                <port id="2" precision="FP32" names="2">
+                    <dim>1</dim>
+                    <dim>3</dim>
+                </port>
+            </output>
+        </layer>
+        <layer id="5" name="2/sink_port_0" type="Result" version="opset1">
+            <input>
+                <port id="0" precision="FP32">
+                    <dim>1</dim>
+                    <dim>3</dim>
+                </port>
+            </input>
+        </layer>
+    </layers>
+    <edges>
+        <edge from-layer="0" from-port="0" to-layer="2" to-port="0" />
+        <edge from-layer="1" from-port="0" to-layer="2" to-port="1" />
+        <edge from-layer="2" from-port="2" to-layer="4" to-port="0" />
+        <edge from-layer="3" from-port="0" to-layer="4" to-port="1" />
+        <edge from-layer="4" from-port="2" to-layer="5" to-port="0" />
+    </edges>
+    <rt_info>
+        <MO_version value="2022.3.0-9052-9752fafe8eb-releases/2022/3" />
+        <Runtime_version value="2022.3.0-9052-9752fafe8eb-releases/2022/3" />
+        <conversion_parameters>
+            <framework value="onnx" />
+            <input_model value="DIR/dummy.onnx" />
+            <model_name value="dummy" />
+        </conversion_parameters>
+        <legacy_frontend value="False" />
+    </rt_info>
+</net>
diff --git a/tests/assets/rise/model_interp.py b/tests/assets/rise/model_interp.py
new file mode 100644
--- /dev/null
+++ b/tests/assets/rise/model_interp.py
@@ -0,0 +1,17 @@
+from datumaro.components.annotation import Label
+from datumaro.util.annotation_util import softmax
+
+
+def process_outputs(inputs, outputs):
+    # inputs = model input; array or images; shape = (B, H, W, C)
+    # outputs = model output; shape = (B, 3);
+    # results = conversion result;
+    # [B x [a score for label0, a score for label1, a score for label2]];
+
+    return [
+        [
+            Label(label=label, attributes={"score": score})
+            for label, score in enumerate(softmax(output))
+        ]
+        for output in outputs
+    ]
diff --git a/tests/integration/cli/test_explain.py b/tests/integration/cli/test_explain.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/cli/test_explain.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2023 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import os
+import os.path as osp
+import platform
+from unittest import TestCase, skipIf
+
+import numpy as np
+
+from datumaro.util.image import save_image
+from datumaro.util.test_utils import TestDir
+from datumaro.util.test_utils import run_datum as run
+
+from ...requirements import Requirements, mark_requirement
+
+from tests.utils.assets import get_test_asset_path
+
+
+class ExplainIntegrationScenarios(TestCase):
+    @skipIf(
+        platform.system() == "Darwin",
+        "Segmentation fault only occurs on MacOS: "
+        "https://github.com/openvinotoolkit/datumaro/actions/runs/4084466205/jobs/7041196279",
+    )
+    @mark_requirement(Requirements.DATUM_BUG_721)
+    def test_rise(self):
+        rise_dir = get_test_asset_path("rise")
+
+        with TestDir() as project_dir:
+            # Create project
+            run(self, "create", "-o", project_dir)
+
+            model_name = "my-model"
+
+            # Add model
+            run(
+                self,
+                "model",
+                "add",
+                "-n",
+                model_name,
+                "-l",
+                "openvino",
+                "-p",
+                project_dir,
+                "--",
+                "-d",
+                osp.join(rise_dir, "model.xml"),
+                "-w",
+                osp.join(rise_dir, "model.bin"),
+                "-i",
+                osp.join(rise_dir, "model_interp.py"),
+            )
+
+            input_fpath = osp.join(project_dir, "input", "input.jpg")
+            save_image(input_fpath, np.zeros([224, 224, 3], dtype=np.uint8), create_dir=True)
+
+            output_dir = osp.join(project_dir, "output")
+
+            # Run explain
+            run(
+                self,
+                "explain",
+                "-m",
+                model_name,
+                "-o",
+                output_dir,
+                "-p",
+                project_dir,
+                input_fpath,
+                "rise",
+            )
+
+            num_heatmap = len(os.listdir(output_dir))
+            self.assertTrue(num_heatmap > 0)
diff --git a/tests/requirements.py b/tests/requirements.py
--- a/tests/requirements.py
+++ b/tests/requirements.py
@@ -59,6 +59,7 @@ class Requirements:
     DATUM_BUG_602 = "Patch command example error"
     DATUM_BUG_606 = "transform with resize also changed the image extension from .jpg to .png"
     DATUM_BUG_618 = "ResizeTransform returns broken image pixels"
+    DATUM_BUG_721 = "Explain command cannot find the model"
 
 
 class SkipMessages: