IVGCVSW-2026 + IVGCVSW-2027 Add FullyConnected Support to TfLiteParser

Change-Id: Id48f97ee33e2fd650a1ee3365ef66bdfc514a586
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fab3d9d..5cdc07d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -414,6 +414,7 @@
              src/armnnTfLiteParser/test/Concatenation.cpp
              src/armnnTfLiteParser/test/Conv2D.cpp
              src/armnnTfLiteParser/test/DepthwiseConvolution2D.cpp
+             src/armnnTfLiteParser/test/FullyConnected.cpp
              src/armnnTfLiteParser/test/MaxPool2D.cpp
              src/armnnTfLiteParser/test/Reshape.cpp
              src/armnnTfLiteParser/test/Softmax.cpp
diff --git a/src/armnnTfLiteParser/TfLiteParser.cpp b/src/armnnTfLiteParser/TfLiteParser.cpp
index 216c090..8b1d3e6 100644
--- a/src/armnnTfLiteParser/TfLiteParser.cpp
+++ b/src/armnnTfLiteParser/TfLiteParser.cpp
@@ -456,6 +456,7 @@
     m_ParserFunctions[tflite::BuiltinOperator_CONCATENATION]     =  &TfLiteParser::ParseConcatenation;
     m_ParserFunctions[tflite::BuiltinOperator_CONV_2D]           =  &TfLiteParser::ParseConv2D;
     m_ParserFunctions[tflite::BuiltinOperator_DEPTHWISE_CONV_2D] =  &TfLiteParser::ParseDepthwiseConv2D;
+    m_ParserFunctions[tflite::BuiltinOperator_FULLY_CONNECTED]   =  &TfLiteParser::ParseFullyConnected;
     m_ParserFunctions[tflite::BuiltinOperator_MAX_POOL_2D]       =  &TfLiteParser::ParseMaxPool2D;
     m_ParserFunctions[tflite::BuiltinOperator_RELU]              =  &TfLiteParser::ParseRelu;
     m_ParserFunctions[tflite::BuiltinOperator_RELU6]             =  &TfLiteParser::ParseRelu6;
@@ -1219,6 +1220,75 @@
     RegisterOutputSlots(subgraphIndex, operatorIndex, layer, {outputTensorIndexes[0]});
 }
 
+void TfLiteParser::ParseFullyConnected(size_t subgraphIndex, size_t operatorIndex)
+{
+    CHECK_MODEL(m_Model, subgraphIndex, operatorIndex);
+
+    const auto & operatorRfr = m_Model->subgraphs[subgraphIndex]->operators[operatorIndex];
+    const auto options = operatorRfr->builtin_options.AsFullyConnectedOptions();
+
+    CHECK_SUPPORTED_FUSED_ACTIVATION(options, subgraphIndex, operatorIndex);
+
+    FullyConnectedDescriptor desc;
+    desc.m_BiasEnabled = false;
+
+    auto inputs = GetInputs(m_Model, subgraphIndex, operatorIndex);
+    auto outputs = GetOutputs(m_Model, subgraphIndex, operatorIndex);
+    CHECK_VALID_SIZE(outputs.size(), 1);
+
+    armnn::TensorInfo filterTensorInfo = ToTensorInfo(inputs[1]);
+
+    // Fully Connected Layer accepts two dimensional weights input
+    int32_t weightsDimension = static_cast<int32_t>(filterTensorInfo.GetNumDimensions());
+    if (weightsDimension != 2)
+    {
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Dimension %1% for Fully Connected weights is not supported by Armnn. "
+                    "Node %2%")
+                % weightsDimension
+                % CHECK_LOCATION().AsString()));
+    }
+
+    auto filterTensorAndData = CreateConstTensor(inputs[1], filterTensorInfo, false);
+    armnn::IConnectableLayer* layer;
+    auto layerName = boost::str(boost::format("FullyConnected:%1%:%2%") % subgraphIndex % operatorIndex);
+
+    if (inputs.size() == 3)
+    {
+        desc.m_BiasEnabled = true;
+        TensorInfo biasTensorInfo = ToTensorInfo(inputs[2]);
+        auto biasTensorAndData = CreateConstTensor(inputs[2], biasTensorInfo, false);
+        layer = m_Network->AddFullyConnectedLayer(desc,
+                                                  filterTensorAndData.first,
+                                                  biasTensorAndData.first,
+                                                  layerName.c_str());
+    }
+    else
+    {
+        layer = m_Network->AddFullyConnectedLayer(desc,
+                                                  filterTensorAndData.first,
+                                                  layerName.c_str());
+    }
+    BOOST_ASSERT(layer != nullptr);
+
+    armnn::TensorInfo outputTensorInfo = ToTensorInfo(outputs[0]);
+    layer->GetOutputSlot(0).SetTensorInfo(outputTensorInfo);
+
+    // register the input connection slot for the layer
+    // only the tensors for the inputs are relevant, exclude the const tensors
+    auto inputTensorIndexes = AsUnsignedVector(GetInputTensorIds(m_Model, subgraphIndex, operatorIndex));
+    RegisterInputSlots(subgraphIndex, operatorIndex, layer, {inputTensorIndexes[0]});
+
+    // we need to add the activation layer and fortunately we don't need to care about the data layout
+    armnn::IConnectableLayer* fusedActivationLayer = AddFusedActivationLayer(layer, 0,
+                                                                             options->fused_activation_function);
+    // register the output connection slots for the layer, connections are made after all layers have been created
+    auto outputTensorIndexes = AsUnsignedVector(GetOutputTensorIds(m_Model, subgraphIndex, operatorIndex));
+    RegisterOutputSlots(subgraphIndex, operatorIndex, fusedActivationLayer, {outputTensorIndexes[0]});
+}
+
 armnn::IConnectableLayer* TfLiteParser::AddFusedActivationLayer(armnn::IConnectableLayer* prevLayer,
                                                                 unsigned int outputSlot,
                                                                 tflite::ActivationFunctionType activationType)
diff --git a/src/armnnTfLiteParser/TfLiteParser.hpp b/src/armnnTfLiteParser/TfLiteParser.hpp
index 35f0b64..76e539a 100644
--- a/src/armnnTfLiteParser/TfLiteParser.hpp
+++ b/src/armnnTfLiteParser/TfLiteParser.hpp
@@ -94,6 +94,7 @@
     void ParseConcatenation(size_t subgraphIndex, size_t operatorIndex);
     void ParseConv2D(size_t subgraphIndex, size_t operatorIndex);
     void ParseDepthwiseConv2D(size_t subgraphIndex, size_t operatorIndex);
+    void ParseFullyConnected(size_t subgraphIndex, size_t operatorIndex);
     void ParseMaxPool2D(size_t subgraphIndex, size_t operatorIndex);
     void ParseRelu(size_t subgraphIndex, size_t operatorIndex);
     void ParseRelu6(size_t subgraphIndex, size_t operatorIndex);
diff --git a/src/armnnTfLiteParser/test/FullyConnected.cpp b/src/armnnTfLiteParser/test/FullyConnected.cpp
new file mode 100644
index 0000000..2853fe9
--- /dev/null
+++ b/src/armnnTfLiteParser/test/FullyConnected.cpp
@@ -0,0 +1,154 @@
+//
+// Copyright © 2017 Arm Ltd. All rights reserved.
+// SPDX-License-Identifier: MIT
+//
+
+#include <boost/test/unit_test.hpp>
+#include "ParserFlatbuffersFixture.hpp"
+#include "../TfLiteParser.hpp"
+
+#include <string>
+#include <iostream>
+
+BOOST_AUTO_TEST_SUITE(TensorflowLiteParser)
+
+struct FullyConnectedFixture : public ParserFlatbuffersFixture
+{
+    explicit FullyConnectedFixture(const std::string& inputShape,
+                                           const std::string& outputShape,
+                                           const std::string& filterShape,
+                                           const std::string& filterData,
+                                           const std::string biasShape = "",
+                                           const std::string biasData = "")
+    {
+        std::string inputTensors = "[ 0, 2 ]";
+        std::string biasTensor = "";
+        std::string biasBuffer = "";
+        if (biasShape.size() > 0 && biasData.size() > 0)
+        {
+            inputTensors = "[ 0, 2, 3 ]";
+            biasTensor = R"(
+                        {
+                            "shape": )" + biasShape + R"( ,
+                            "type": "INT32",
+                            "buffer": 3,
+                            "name": "biasTensor",
+                            "quantization": {
+                                "min": [ 0.0 ],
+                                "max": [ 255.0 ],
+                                "scale": [ 1.0 ],
+                                "zero_point": [ 0 ],
+                            }
+                        } )";
+            biasBuffer = R"(
+                    { "data": )" + biasData + R"(, }, )";
+        }
+        m_JsonString = R"(
+            {
+                "version": 3,
+                "operator_codes": [ { "builtin_code": "FULLY_CONNECTED" } ],
+                "subgraphs": [ {
+                    "tensors": [
+                        {
+                            "shape": )" + inputShape + R"(,
+                            "type": "UINT8",
+                            "buffer": 0,
+                            "name": "inputTensor",
+                            "quantization": {
+                                "min": [ 0.0 ],
+                                "max": [ 255.0 ],
+                                "scale": [ 1.0 ],
+                                "zero_point": [ 0 ],
+                            }
+                        },
+                        {
+                            "shape": )" + outputShape + R"(,
+                            "type": "UINT8",
+                            "buffer": 1,
+                            "name": "outputTensor",
+                            "quantization": {
+                                "min": [ 0.0 ],
+                                "max": [ 511.0 ],
+                                "scale": [ 2.0 ],
+                                "zero_point": [ 0 ],
+                            }
+                        },
+                        {
+                            "shape": )" + filterShape + R"(,
+                            "type": "UINT8",
+                            "buffer": 2,
+                            "name": "filterTensor",
+                            "quantization": {
+                                "min": [ 0.0 ],
+                                "max": [ 255.0 ],
+                                "scale": [ 1.0 ],
+                                "zero_point": [ 0 ],
+                            }
+                        }, )" + biasTensor + R"(
+                    ],
+                    "inputs": [ 0 ],
+                    "outputs": [ 1 ],
+                    "operators": [
+                        {
+                            "opcode_index": 0,
+                            "inputs": )" + inputTensors + R"(,
+                            "outputs": [ 1 ],
+                            "builtin_options_type": "FullyConnectedOptions",
+                            "builtin_options": {
+                                "fused_activation_function": "NONE"
+                            },
+                            "custom_options_format": "FLEXBUFFERS"
+                        }
+                    ],
+                } ],
+                "buffers" : [
+                    { },
+                    { },
+                    { "data": )" + filterData + R"(, }, )"
+                       + biasBuffer + R"(
+                ]
+            }
+        )";
+        SetupSingleInputSingleOutput("inputTensor", "outputTensor");
+    }
+};
+
+struct FullyConnectedWithNoBiasFixture : FullyConnectedFixture
+{
+    FullyConnectedWithNoBiasFixture()
+        : FullyConnectedFixture("[ 1, 4, 1, 1 ]",     // inputShape
+                                "[ 1, 1 ]",           // outputShape
+                                "[ 4, 1 ]",           // filterShape
+                                "[ 2, 3, 4, 5 ]")     // filterData
+    {}
+};
+
+BOOST_FIXTURE_TEST_CASE(FullyConnectedWithNoBias, FullyConnectedWithNoBiasFixture)
+{
+    RunTest<2, uint8_t>(
+        0,
+        { 10, 20, 30, 40 },
+        { 400/2 });
+}
+
+struct FullyConnectedWithBiasFixture : FullyConnectedFixture
+{
+    FullyConnectedWithBiasFixture()
+        : FullyConnectedFixture("[ 1, 4, 1, 1 ]",     // inputShape
+                                "[ 1, 1 ]",           // outputShape
+                                "[ 4, 1 ]",           // filterShape
+                                "[ 2, 3, 4, 5 ]",     // filterData
+                                "[ 1 ]",              // biasShape
+                                "[ 10, 0, 0, 0 ]" )   // biasData
+    {}
+};
+
+BOOST_FIXTURE_TEST_CASE(ParseFullyConnectedWithBias, FullyConnectedWithBiasFixture)
+{
+    RunTest<2, uint8_t>(
+        0,
+        { 10, 20, 30, 40 },
+        { (400+10)/2 });
+}
+
+BOOST_AUTO_TEST_SUITE_END()