IVGCVSW-2437 Inference test for TensorFlow Lite MobileNet SSD

Change-Id: If7ee1efa3ee79d9eca41c5a6219b3fc42e740efe
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 9815537..e8f72eb 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -164,6 +164,13 @@
         ImagePreprocessor.cpp)
     TfLiteParserTest(TfLiteMobilenetQuantized-Armnn "${TfLiteMobilenetQuantized-Armnn_sources}")
 
+    set(TfLiteMobileNetSsd-Armnn_sources
+        TfLiteMobileNetSsd-Armnn/TfLiteMobileNetSsd-Armnn.cpp
+        MobileNetSsdDatabase.hpp
+        MobileNetSsdInferenceTest.hpp
+        ObjectDetectionCommon.hpp)
+    TfLiteParserTest(TfLiteMobileNetSsd-Armnn "${TfLiteMobileNetSsd-Armnn_sources}")
+
     set(TfLiteVGG16Quantized-Armnn_sources
         TfLiteVGG16Quantized-Armnn/TfLiteVGG16Quantized-Armnn.cpp
         ImagePreprocessor.hpp
diff --git a/tests/MobileNetSsdDatabase.hpp b/tests/MobileNetSsdDatabase.hpp
new file mode 100644
index 0000000..e3a28d1
--- /dev/null
+++ b/tests/MobileNetSsdDatabase.hpp
@@ -0,0 +1,105 @@
+//
+// Copyright © 2017 Arm Ltd. All rights reserved.
+// SPDX-License-Identifier: MIT
+//
+#pragma once
+
+#include "ObjectDetectionCommon.hpp"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <armnn/TypesUtils.hpp>
+
+#include <boost/log/trivial.hpp>
+#include <boost/numeric/conversion/cast.hpp>
+
+#include <array>
+#include <string>
+
+#include "InferenceTestImage.hpp"
+
+namespace
+{
+
+struct MobileNetSsdTestCaseData
+{
+    MobileNetSsdTestCaseData(
+        std::vector<float> inputData,
+        std::vector<DetectedObject> expectedOutput)
+        : m_InputData(std::move(inputData))
+        , m_ExpectedOutput(std::move(expectedOutput))
+    {}
+
+    std::vector<float>          m_InputData;
+    std::vector<DetectedObject> m_ExpectedOutput;
+};
+
+class MobileNetSsdDatabase
+{
+public:
+    explicit MobileNetSsdDatabase(const std::string& imageDir);
+
+    std::unique_ptr<MobileNetSsdTestCaseData> GetTestCaseData(unsigned int testCaseId);
+
+private:
+    std::string m_ImageDir;
+};
+
+constexpr unsigned int k_MobileNetSsdImageWidth  = 300u;
+constexpr unsigned int k_MobileNetSsdImageHeight = k_MobileNetSsdImageWidth;
+
+// Test cases
+const std::array<ObjectDetectionInput, 1> g_PerTestCaseInput =
+{
+    ObjectDetectionInput
+    {
+        "Cat.jpg",
+        DetectedObject(16, BoundingBox(0.21678525f, 0.0859828f, 0.9271242f, 0.9453231f), 0.79296875f)
+    }
+};
+
+MobileNetSsdDatabase::MobileNetSsdDatabase(const std::string& imageDir)
+    : m_ImageDir(imageDir)
+{}
+
+std::unique_ptr<MobileNetSsdTestCaseData> MobileNetSsdDatabase::GetTestCaseData(unsigned int testCaseId)
+{
+    const unsigned int safeTestCaseId =
+        testCaseId % boost::numeric_cast<unsigned int>(g_PerTestCaseInput.size());
+    const ObjectDetectionInput& testCaseInput = g_PerTestCaseInput[safeTestCaseId];
+
+    // Load test case input
+    const std::string imagePath = m_ImageDir + testCaseInput.first;
+    std::vector<float> imageData;
+    try
+    {
+        InferenceTestImage image(imagePath.c_str());
+
+        // Resize image (if needed)
+        const unsigned int width  = image.GetWidth();
+        const unsigned int height = image.GetHeight();
+        if (width != k_MobileNetSsdImageWidth || height != k_MobileNetSsdImageHeight)
+        {
+            image.Resize(k_MobileNetSsdImageWidth, k_MobileNetSsdImageHeight, CHECK_LOCATION());
+        }
+
+        // Get image data as a vector of floats
+        imageData = GetImageDataInArmNnLayoutAsNormalizedFloats(ImageChannelLayout::Rgb, image);
+    }
+    catch (const InferenceTestImageException& e)
+    {
+        BOOST_LOG_TRIVIAL(fatal) << "Failed to load image for test case " << testCaseId << ". Error: " << e.what();
+        return nullptr;
+    }
+
+    // Prepare test case expected output
+    std::vector<DetectedObject> expectedOutput;
+    expectedOutput.reserve(1);
+    expectedOutput.push_back(testCaseInput.second);
+
+    return std::make_unique<MobileNetSsdTestCaseData>(std::move(imageData), std::move(expectedOutput));
+}
+
+} // anonymous namespace
diff --git a/tests/MobileNetSsdInferenceTest.hpp b/tests/MobileNetSsdInferenceTest.hpp
new file mode 100644
index 0000000..cf00966
--- /dev/null
+++ b/tests/MobileNetSsdInferenceTest.hpp
@@ -0,0 +1,202 @@
+//
+// Copyright © 2017 Arm Ltd. All rights reserved.
+// SPDX-License-Identifier: MIT
+//
+#pragma once
+
+#include "InferenceTest.hpp"
+#include "MobileNetSsdDatabase.hpp"
+
+#include <boost/assert.hpp>
+#include <boost/log/trivial.hpp>
+#include <boost/numeric/conversion/cast.hpp>
+#include <boost/test/tools/floating_point_comparison.hpp>
+
+#include <vector>
+
+namespace
+{
+
+template<typename Model>
+class MobileNetSsdTestCase : public InferenceModelTestCase<Model>
+{
+public:
+    MobileNetSsdTestCase(Model& model,
+                         unsigned int testCaseId,
+                         const MobileNetSsdTestCaseData& testCaseData)
+        : InferenceModelTestCase<Model>(model,
+                                        testCaseId,
+                                        { std::move(testCaseData.m_InputData) },
+                                        { k_OutputSize1, k_OutputSize2, k_OutputSize3, k_OutputSize4 })
+        , m_FloatComparer(boost::math::fpc::percent_tolerance(1.0f))
+        , m_DetectedObjects(testCaseData.m_ExpectedOutput)
+    {}
+
+    TestCaseResult ProcessResult(const InferenceTestOptions& options) override
+    {
+        const std::vector<float>& output1 = this->GetOutputs()[0]; // bounding boxes
+        BOOST_ASSERT(output1.size() == k_OutputSize1);
+
+        const std::vector<float>& output2 = this->GetOutputs()[1]; // classes
+        BOOST_ASSERT(output2.size() == k_OutputSize2);
+
+        const std::vector<float>& output3 = this->GetOutputs()[2]; // scores
+        BOOST_ASSERT(output3.size() == k_OutputSize3);
+
+        const std::vector<float>& output4 = this->GetOutputs()[3]; // number of valid detections
+        BOOST_ASSERT(output4.size() == k_OutputSize4);
+
+        // Extract detected objects from output data
+        std::vector<DetectedObject> detectedObjects;
+        const float* outputData = output1.data();
+        for (unsigned int i = 0u; i < k_NumDetections; i++)
+        {
+            // NOTE: Order of coordinates in output data is yMin, xMin, yMax, xMax
+            float yMin = *outputData++;
+            float xMin = *outputData++;
+            float yMax = *outputData++;
+            float xMax = *outputData++;
+
+            DetectedObject detectedObject(
+                static_cast<unsigned int>(output2.at(i)),
+                BoundingBox(xMin, yMin, xMax, yMax),
+                output3.at(i));
+
+            detectedObjects.push_back(detectedObject);
+        }
+
+        // Sort detected objects by confidence
+        std::sort(detectedObjects.begin(), detectedObjects.end(),
+            [](const DetectedObject& a, const DetectedObject& b)
+            {
+                return a.m_Confidence > b.m_Confidence ||
+                    (a.m_Confidence == b.m_Confidence && a.m_Class > b.m_Class);
+            });
+
+        // Check if number of valid detections matches expectations
+        const size_t numValidDetections = boost::numeric_cast<size_t>(output4[0]);
+        if (numValidDetections != m_DetectedObjects.size())
+        {
+            BOOST_LOG_TRIVIAL(error) << "Number of valid detections is incorrect: Expected (" <<
+                m_DetectedObjects.size() << ")" << " but got (" << numValidDetections << ")";
+            return TestCaseResult::Failed;
+        }
+
+        // Compare detected objects with expected results
+        std::vector<DetectedObject>::const_iterator it = detectedObjects.begin();
+        for (const DetectedObject& expectedDetection : m_DetectedObjects)
+        {
+            if (it == detectedObjects.end())
+            {
+                BOOST_LOG_TRIVIAL(info) << "No more detected objects to compare";
+                return TestCaseResult::Abort;
+            }
+
+            const DetectedObject& detectedObject = *it;
+            if (detectedObject.m_Class != expectedDetection.m_Class)
+            {
+                BOOST_LOG_TRIVIAL(error) << "Prediction for test case " << this->GetTestCaseId() <<
+                    " is incorrect: Expected (" << expectedDetection.m_Class << ")" <<
+                    " but predicted (" << detectedObject.m_Class << ")";
+                return TestCaseResult::Failed;
+            }
+
+            if(!m_FloatComparer(detectedObject.m_Confidence, expectedDetection.m_Confidence))
+            {
+                BOOST_LOG_TRIVIAL(error) << "Confidence of prediction for test case " << this->GetTestCaseId() <<
+                    " is incorrect: Expected (" << expectedDetection.m_Confidence << ")  +- 1.0 pc" <<
+                    " but predicted (" << detectedObject.m_Confidence << ")";
+                return TestCaseResult::Failed;
+            }
+
+            if (!m_FloatComparer(detectedObject.m_BoundingBox.m_XMin, expectedDetection.m_BoundingBox.m_XMin) ||
+                !m_FloatComparer(detectedObject.m_BoundingBox.m_YMin, expectedDetection.m_BoundingBox.m_YMin) ||
+                !m_FloatComparer(detectedObject.m_BoundingBox.m_XMax, expectedDetection.m_BoundingBox.m_XMax) ||
+                !m_FloatComparer(detectedObject.m_BoundingBox.m_YMax, expectedDetection.m_BoundingBox.m_YMax))
+            {
+                BOOST_LOG_TRIVIAL(error) << "Detected bounding box for test case " << this->GetTestCaseId() <<
+                    " is incorrect";
+                return TestCaseResult::Failed;
+            }
+
+            ++it;
+        }
+
+        return TestCaseResult::Ok;
+    }
+
+private:
+    static constexpr unsigned int k_NumDetections = 10u;
+
+    static constexpr unsigned int k_OutputSize1 = k_NumDetections * 4u;
+    static constexpr unsigned int k_OutputSize2 = k_NumDetections;
+    static constexpr unsigned int k_OutputSize3 = k_NumDetections;
+    static constexpr unsigned int k_OutputSize4 = 1u;
+
+    boost::math::fpc::close_at_tolerance<float> m_FloatComparer;
+    std::vector<DetectedObject>                 m_DetectedObjects;
+};
+
+template <typename Model>
+class MobileNetSsdTestCaseProvider : public IInferenceTestCaseProvider
+{
+public:
+    template <typename TConstructModelCallable>
+    explicit MobileNetSsdTestCaseProvider(TConstructModelCallable constructModel)
+        : m_ConstructModel(constructModel)
+    {}
+
+    virtual void AddCommandLineOptions(boost::program_options::options_description& options) override
+    {
+        namespace po = boost::program_options;
+
+        options.add_options()
+            ("data-dir,d", po::value<std::string>(&m_DataDir)->required(),
+                "Path to directory containing test data");
+
+        Model::AddCommandLineOptions(options, m_ModelCommandLineOptions);
+    }
+
+    virtual bool ProcessCommandLineOptions() override
+    {
+        if (!ValidateDirectory(m_DataDir))
+        {
+            return false;
+        }
+
+        m_Model = m_ConstructModel(m_ModelCommandLineOptions);
+        if (!m_Model)
+        {
+            return false;
+        }
+
+        m_Database = std::make_unique<MobileNetSsdDatabase>(m_DataDir.c_str());
+        if (!m_Database)
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    std::unique_ptr<IInferenceTestCase> GetTestCase(unsigned int testCaseId) override
+    {
+        std::unique_ptr<MobileNetSsdTestCaseData> testCaseData = m_Database->GetTestCaseData(testCaseId);
+        if (!testCaseData)
+        {
+            return nullptr;
+        }
+
+        return std::make_unique<MobileNetSsdTestCase<Model>>(*m_Model, testCaseId, *testCaseData);
+    }
+
+private:
+    typename Model::CommandLineOptions m_ModelCommandLineOptions;
+    std::function<std::unique_ptr<Model>(typename Model::CommandLineOptions)> m_ConstructModel;
+    std::unique_ptr<Model> m_Model;
+
+    std::string m_DataDir;
+    std::unique_ptr<MobileNetSsdDatabase> m_Database;
+};
+
+} // anonymous namespace
\ No newline at end of file
diff --git a/tests/ObjectDetectionCommon.hpp b/tests/ObjectDetectionCommon.hpp
new file mode 100644
index 0000000..85b54c2
--- /dev/null
+++ b/tests/ObjectDetectionCommon.hpp
@@ -0,0 +1,49 @@
+//
+// Copyright © 2017 Arm Ltd. All rights reserved.
+// SPDX-License-Identifier: MIT
+//
+#pragma once
+
+#include <string>
+#include <utility>
+
+namespace
+{
+
+struct BoundingBox
+{
+    BoundingBox()
+        : BoundingBox(0.0f, 0.0f, 0.0f, 0.0f)
+    {}
+
+    BoundingBox(float xMin, float yMin, float xMax, float yMax)
+        : m_XMin(xMin)
+        , m_YMin(yMin)
+        , m_XMax(xMax)
+        , m_YMax(yMax)
+    {}
+
+    float m_XMin;
+    float m_YMin;
+    float m_XMax;
+    float m_YMax;
+};
+
+struct DetectedObject
+{
+    DetectedObject(unsigned int detectedClass,
+                   const BoundingBox& boundingBox,
+                   float confidence)
+        : m_Class(detectedClass)
+        , m_BoundingBox(boundingBox)
+        , m_Confidence(confidence)
+    {}
+
+    unsigned int m_Class;
+    BoundingBox  m_BoundingBox;
+    float        m_Confidence;
+};
+
+using ObjectDetectionInput = std::pair<std::string, DetectedObject>;
+
+} // anonymous namespace
\ No newline at end of file
diff --git a/tests/TfLiteMobileNetSsd-Armnn/TfLiteMobileNetSsd-Armnn.cpp b/tests/TfLiteMobileNetSsd-Armnn/TfLiteMobileNetSsd-Armnn.cpp
new file mode 100644
index 0000000..b1bc0f6
--- /dev/null
+++ b/tests/TfLiteMobileNetSsd-Armnn/TfLiteMobileNetSsd-Armnn.cpp
@@ -0,0 +1,76 @@
+//
+// Copyright © 2017 Arm Ltd. All rights reserved.
+// SPDX-License-Identifier: MIT
+//
+#include "../MobileNetSsdInferenceTest.hpp"
+
+#include "armnnTfLiteParser/ITfLiteParser.hpp"
+
+#include <algorithm>
+#include <iterator>
+
+using namespace armnnTfLiteParser;
+
+int main(int argc, char* argv[])
+{
+    int retVal = EXIT_FAILURE;
+    try
+    {
+        using DataType = float;
+        using Parser   = armnnTfLiteParser::ITfLiteParser;
+        using Model    = InferenceModel<Parser, DataType>;
+
+        armnn::TensorShape inputTensorShape({ 1, 300, 300, 3  });
+
+        std::vector<const char*> inputLayerNames  =
+        {
+            "normalized_input_image_tensor"
+        };
+
+        std::vector<const char*> outputLayerNames =
+        {
+            "TFLite_Detection_PostProcess",
+            "TFLite_Detection_PostProcess:1",
+            "TFLite_Detection_PostProcess:2",
+            "TFLite_Detection_PostProcess:3"
+        };
+
+        retVal = InferenceTestMain(argc, argv, { 0 },
+            [&inputTensorShape, inputLayerNames, outputLayerNames]()
+            {
+                return make_unique<MobileNetSsdTestCaseProvider<Model>>(
+                    [&]
+                    (typename Model::CommandLineOptions modelOptions)
+                    {
+                        if (!ValidateDirectory(modelOptions.m_ModelDir))
+                        {
+                            return std::unique_ptr<Model>();
+                        }
+
+                        typename Model::Params modelParams;
+                        modelParams.m_ModelPath =
+                            modelOptions.m_ModelDir + "ssd_mobilenet_v1.tflite";
+
+                        std::copy(inputLayerNames.begin(), inputLayerNames.end(),
+                                  std::back_inserter(modelParams.m_InputBindings));
+
+                        std::copy(outputLayerNames.begin(), outputLayerNames.end(),
+                                  std::back_inserter(modelParams.m_OutputBindings));
+
+                        modelParams.m_InputShapes                    = { inputTensorShape };
+                        modelParams.m_IsModelBinary                  = true;
+                        modelParams.m_ComputeDevice                  = modelOptions.m_ComputeDevice;
+                        modelParams.m_VisualizePostOptimizationModel = modelOptions.m_VisualizePostOptimizationModel;
+                        modelParams.m_EnableFp16TurboMode            = modelOptions.m_EnableFp16TurboMode;
+
+                        return std::make_unique<Model>(modelParams);
+                });
+            });
+    }
+    catch (const std::exception& e)
+    {
+        std::cerr << "WARNING: " << *argv << ": An error has occurred when running "
+                     "the classifier inference tests: " << e.what() << std::endl;
+    }
+    return retVal;
+}