xref: /aosp_15_r20/external/armnn/tests/TfLiteYoloV3Big-Armnn/TfLiteYoloV3Big-Armnn.cpp (revision 89c4ff92f2867872bb9e2354d150bf0c8c502810)
1 //
2 // Copyright © 2020 Arm Ltd. All rights reserved.
3 // SPDX-License-Identifier: MIT
4 //
5 
6 #include "armnnTfLiteParser/ITfLiteParser.hpp"
7 
8 #include "NMS.hpp"
9 
10 #include <stb/stb_image.h>
11 
12 #include <armnn/INetwork.hpp>
13 #include <armnn/IRuntime.hpp>
14 #include <armnn/Logging.hpp>
15 #include <armnn/utility/IgnoreUnused.hpp>
16 
17 #include <cxxopts/cxxopts.hpp>
18 #include <ghc/filesystem.hpp>
19 
20 #include <chrono>
21 #include <fstream>
22 #include <iostream>
23 #include <iterator>
24 #include <cmath>
25 
26 using namespace armnnTfLiteParser;
27 using namespace armnn;
28 
29 static const int OPEN_FILE_ERROR = -2;
30 static const int OPTIMIZE_NETWORK_ERROR = -3;
31 static const int LOAD_NETWORK_ERROR = -4;
32 static const int LOAD_IMAGE_ERROR = -5;
33 static const int GENERAL_ERROR = -100;
34 
35 #define CHECK_OK(v)                                     \
36     do {                                                \
37         try {                                           \
38             auto r_local = v;                           \
39             if (r_local != 0) { return r_local;}        \
40         }                                               \
41         catch (const armnn::Exception& e)               \
42         {                                               \
43             ARMNN_LOG(error) << "Oops: " << e.what();   \
44             return GENERAL_ERROR;                       \
45         }                                               \
46     } while(0)
47 
48 
49 
50 template<typename TContainer>
MakeInputTensors(const std::vector<armnn::BindingPointInfo> & inputBindings,const std::vector<std::reference_wrapper<TContainer>> & inputDataContainers)51 inline armnn::InputTensors MakeInputTensors(const std::vector<armnn::BindingPointInfo>& inputBindings,
52                                             const std::vector<std::reference_wrapper<TContainer>>& inputDataContainers)
53 {
54     armnn::InputTensors inputTensors;
55 
56     const size_t numInputs = inputBindings.size();
57     if (numInputs != inputDataContainers.size())
58     {
59         throw armnn::Exception("Mismatching vectors");
60     }
61 
62     for (size_t i = 0; i < numInputs; i++)
63     {
64         const armnn::BindingPointInfo& inputBinding = inputBindings[i];
65         const TContainer& inputData = inputDataContainers[i].get();
66 
67         armnn::ConstTensor inputTensor(inputBinding.second, inputData.data());
68         inputTensors.push_back(std::make_pair(inputBinding.first, inputTensor));
69     }
70 
71     return inputTensors;
72 }
73 
74 template<typename TContainer>
MakeOutputTensors(const std::vector<armnn::BindingPointInfo> & outputBindings,const std::vector<std::reference_wrapper<TContainer>> & outputDataContainers)75 inline armnn::OutputTensors MakeOutputTensors(
76     const std::vector<armnn::BindingPointInfo>& outputBindings,
77     const std::vector<std::reference_wrapper<TContainer>>& outputDataContainers)
78 {
79     armnn::OutputTensors outputTensors;
80 
81     const size_t numOutputs = outputBindings.size();
82     if (numOutputs != outputDataContainers.size())
83     {
84         throw armnn::Exception("Mismatching vectors");
85     }
86 
87     outputTensors.reserve(numOutputs);
88 
89     for (size_t i = 0; i < numOutputs; i++)
90     {
91         const armnn::BindingPointInfo& outputBinding = outputBindings[i];
92         const TContainer& outputData = outputDataContainers[i].get();
93 
94         armnn::Tensor outputTensor(outputBinding.second, const_cast<float*>(outputData.data()));
95         outputTensors.push_back(std::make_pair(outputBinding.first, outputTensor));
96     }
97 
98     return outputTensors;
99 }
100 
101 #define S_BOOL(name) enum class name {False=0, True=1};
102 
103 S_BOOL(ImportMemory)
S_BOOL(DumpToDot)104 S_BOOL(DumpToDot)
105 S_BOOL(ExpectFile)
106 S_BOOL(OptionalArg)
107 
108 int LoadModel(const char* filename,
109               ITfLiteParser& parser,
110               IRuntime& runtime,
111               NetworkId& networkId,
112               const std::vector<BackendId>& backendPreferences,
113               ImportMemory enableImport,
114               DumpToDot dumpToDot)
115 {
116     std::ifstream stream(filename, std::ios::in | std::ios::binary);
117     if (!stream.is_open())
118     {
119         ARMNN_LOG(error) << "Could not open model: " << filename;
120         return OPEN_FILE_ERROR;
121     }
122 
123     std::vector<uint8_t> contents((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
124     stream.close();
125 
126     auto model = parser.CreateNetworkFromBinary(contents);
127     contents.clear();
128     ARMNN_LOG(debug) << "Model loaded ok: " << filename;
129 
130     // Optimize backbone model
131     OptimizerOptionsOpaque options;
132     options.SetImportEnabled(enableImport != ImportMemory::False);
133     auto optimizedModel = Optimize(*model, backendPreferences, runtime.GetDeviceSpec(), options);
134     if (!optimizedModel)
135     {
136         ARMNN_LOG(fatal) << "Could not optimize the model:" << filename;
137         return OPTIMIZE_NETWORK_ERROR;
138     }
139 
140     if (dumpToDot != DumpToDot::False)
141     {
142         std::stringstream ss;
143         ss << filename << ".dot";
144         std::ofstream dotStream(ss.str().c_str(), std::ofstream::out);
145         optimizedModel->SerializeToDot(dotStream);
146         dotStream.close();
147     }
148     // Load model into runtime
149     {
150         std::string errorMessage;
151 
152         armnn::MemorySource memSource = options.GetImportEnabled() ? armnn::MemorySource::Malloc
153                                                                 : armnn::MemorySource::Undefined;
154         INetworkProperties modelProps(false, memSource, memSource);
155         Status status = runtime.LoadNetwork(networkId, std::move(optimizedModel), errorMessage, modelProps);
156         if (status != Status::Success)
157         {
158             ARMNN_LOG(fatal) << "Could not load " << filename << " model into runtime: " << errorMessage;
159             return LOAD_NETWORK_ERROR;
160         }
161     }
162 
163     return 0;
164 }
165 
LoadImage(const char * filename)166 std::vector<float> LoadImage(const char* filename)
167 {
168     if (strlen(filename) == 0)
169     {
170         return std::vector<float>(1920*10180*3, 0.0f);
171     }
172     struct Memory
173     {
174         ~Memory() {stbi_image_free(m_Data);}
175         bool IsLoaded() const { return m_Data != nullptr;}
176 
177         unsigned char* m_Data;
178     };
179 
180     std::vector<float> image;
181 
182     int width;
183     int height;
184     int channels;
185 
186     Memory mem = {stbi_load(filename, &width, &height, &channels, 3)};
187     if (!mem.IsLoaded())
188     {
189         ARMNN_LOG(error) << "Could not load input image file: " << filename;
190         return image;
191     }
192 
193     if (width != 1920 || height != 1080 || channels != 3)
194     {
195         ARMNN_LOG(error) << "Input image has wong dimension: " << width << "x" << height << "x" << channels << ". "
196           " Expected 1920x1080x3.";
197         return image;
198     }
199 
200     image.resize(1920*1080*3);
201 
202     // Expand to float. Does this need de-gamma?
203     for (unsigned int idx=0; idx <= 1920*1080*3; idx++)
204     {
205         image[idx] = static_cast<float>(mem.m_Data[idx]) /255.0f;
206     }
207 
208     return image;
209 }
210 
211 
ValidateFilePath(std::string & file,ExpectFile expectFile)212 bool ValidateFilePath(std::string& file, ExpectFile expectFile)
213 {
214     if (!ghc::filesystem::exists(file))
215     {
216         std::cerr << "Given file path " << file << " does not exist" << std::endl;
217         return false;
218     }
219     if (!ghc::filesystem::is_regular_file(file) && expectFile == ExpectFile::True)
220     {
221         std::cerr << "Given file path " << file << " is not a regular file" << std::endl;
222         return false;
223     }
224     return true;
225 }
226 
CheckAccuracy(std::vector<float> * toDetector0,std::vector<float> * toDetector1,std::vector<float> * toDetector2,std::vector<float> * detectorOutput,const std::vector<yolov3::Detection> & nmsOut,const std::vector<std::string> & filePaths)227 void CheckAccuracy(std::vector<float>* toDetector0, std::vector<float>* toDetector1,
228                    std::vector<float>* toDetector2, std::vector<float>* detectorOutput,
229                    const std::vector<yolov3::Detection>& nmsOut, const std::vector<std::string>& filePaths)
230 {
231     std::ifstream pathStream;
232     std::vector<float> expected;
233     std::vector<std::vector<float>*> outputs;
234     float compare = 0;
235     unsigned int count = 0;
236 
237     //Push back output vectors from inference for use in loop
238     outputs.push_back(toDetector0);
239     outputs.push_back(toDetector1);
240     outputs.push_back(toDetector2);
241     outputs.push_back(detectorOutput);
242 
243     for (unsigned int i = 0; i < outputs.size(); ++i)
244     {
245         // Reading expected output files and assigning them to @expected. Close and Clear to reuse stream and clean RAM
246         pathStream.open(filePaths[i]);
247         if (!pathStream.is_open())
248         {
249             ARMNN_LOG(error) << "Expected output file can not be opened: " << filePaths[i];
250             continue;
251         }
252 
253         expected.assign(std::istream_iterator<float>(pathStream), {});
254         pathStream.close();
255         pathStream.clear();
256 
257         // Ensure each vector is the same length
258         if (expected.size() != outputs[i]->size())
259         {
260             ARMNN_LOG(error) << "Expected output size does not match actual output size: " << filePaths[i];
261         }
262         else
263         {
264             count = 0;
265 
266             // Compare abs(difference) with tolerance to check for value by value equality
267             for (unsigned int j = 0; j < outputs[i]->size(); ++j)
268             {
269                 compare = std::abs(expected[j] - outputs[i]->at(j));
270                 if (compare > 0.001f)
271                 {
272                     count++;
273                 }
274             }
275             if (count > 0)
276             {
277                 ARMNN_LOG(error) << count << " output(s) do not match expected values in: " << filePaths[i];
278             }
279         }
280     }
281 
282     pathStream.open(filePaths[4]);
283     if (!pathStream.is_open())
284     {
285         ARMNN_LOG(error) << "Expected output file can not be opened: " << filePaths[4];
286     }
287     else
288     {
289         expected.assign(std::istream_iterator<float>(pathStream), {});
290         pathStream.close();
291         pathStream.clear();
292         unsigned int y = 0;
293         unsigned int numOfMember = 6;
294         std::vector<float> intermediate;
295 
296         for (auto& detection: nmsOut)
297         {
298             for (unsigned int x = y * numOfMember; x < ((y * numOfMember) + numOfMember); ++x)
299             {
300                 intermediate.push_back(expected[x]);
301             }
302             if (!yolov3::compare_detection(detection, intermediate))
303             {
304                 ARMNN_LOG(error) << "Expected NMS output does not match: Detection " << y + 1;
305             }
306             intermediate.clear();
307             y++;
308         }
309     }
310 }
311 
312 struct ParseArgs
313 {
ParseArgsParseArgs314     ParseArgs(int ac, char *av[]) : options{"TfLiteYoloV3Big-Armnn",
315                                             "Executes YoloV3Big using ArmNN. YoloV3Big consists "
316                                             "of 3 parts: A backbone TfLite model, a detector TfLite "
317                                             "model, and None Maximum Suppression. All parts are "
318                                             "executed successively."}
319     {
320         options.add_options()
321                 ("b,backbone-path",
322                  "File path where the TfLite model for the yoloV3big backbone "
323                  "can be found e.g. mydir/yoloV3big_backbone.tflite",
324                  cxxopts::value<std::string>())
325 
326                ("c,comparison-files",
327                 "Defines the expected outputs for the model "
328                 "of yoloV3big e.g. 'mydir/file1.txt,mydir/file2.txt,mydir/file3.txt,mydir/file4.txt'->InputToDetector1"
329                 " will be tried first then InputToDetector2 then InputToDetector3 then the Detector Output and finally"
330                 " the NMS output. NOTE: Files are passed as comma separated list without whitespaces.",
331                 cxxopts::value<std::vector<std::string>>()->default_value({}))
332 
333                 ("d,detector-path",
334                  "File path where the TfLite model for the yoloV3big "
335                  "detector can be found e.g.'mydir/yoloV3big_detector.tflite'",
336                  cxxopts::value<std::string>())
337 
338                 ("h,help", "Produce help message")
339 
340                 ("i,image-path",
341                  "File path to a 1080x1920 jpg image that should be "
342                  "processed e.g. 'mydir/example_img_180_1920.jpg'",
343                  cxxopts::value<std::string>())
344 
345                 ("B,preferred-backends-backbone",
346                  "Defines the preferred backends to run the backbone model "
347                  "of yoloV3big e.g. 'GpuAcc,CpuRef' -> GpuAcc will be tried "
348                  "first before falling back to CpuRef. NOTE: Backends are passed "
349                  "as comma separated list without whitespaces.",
350                  cxxopts::value<std::vector<std::string>>()->default_value("GpuAcc,CpuRef"))
351 
352                 ("D,preferred-backends-detector",
353                  "Defines the preferred backends to run the detector model "
354                  "of yoloV3big e.g. 'CpuAcc,CpuRef' -> CpuAcc will be tried "
355                  "first before falling back to CpuRef. NOTE: Backends are passed "
356                  "as comma separated list without whitespaces.",
357                  cxxopts::value<std::vector<std::string>>()->default_value("CpuAcc,CpuRef"))
358 
359                 ("M, model-to-dot",
360                  "Dump the optimized model to a dot file for debugging/analysis",
361                  cxxopts::value<bool>()->default_value("false"))
362 
363                 ("Y, dynamic-backends-path",
364                  "Define a path from which to load any dynamic backends.",
365                  cxxopts::value<std::string>());
366 
367         auto result = options.parse(ac, av);
368 
369         if (result.count("help"))
370         {
371             std::cout << options.help() << "\n";
372             exit(EXIT_SUCCESS);
373         }
374 
375 
376         backboneDir = GetPathArgument(result, "backbone-path", ExpectFile::True, OptionalArg::False);
377 
378         comparisonFiles = GetPathArgument(result["comparison-files"].as<std::vector<std::string>>(), OptionalArg::True);
379 
380         detectorDir = GetPathArgument(result, "detector-path", ExpectFile::True, OptionalArg::False);
381 
382         imageDir    = GetPathArgument(result, "image-path", ExpectFile::True, OptionalArg::True);
383 
384         dynamicBackendPath = GetPathArgument(result, "dynamic-backends-path", ExpectFile::False, OptionalArg::True);
385 
386         prefBackendsBackbone = GetBackendIDs(result["preferred-backends-backbone"].as<std::vector<std::string>>());
387         LogBackendsInfo(prefBackendsBackbone, "Backbone");
388         prefBackendsDetector = GetBackendIDs(result["preferred-backends-detector"].as<std::vector<std::string>>());
389         LogBackendsInfo(prefBackendsDetector, "detector");
390 
391         dumpToDot = result["model-to-dot"].as<bool>() ? DumpToDot::True : DumpToDot::False;
392     }
393 
394     /// Takes a vector of backend strings and returns a vector of backendIDs
GetBackendIDsParseArgs395     std::vector<BackendId> GetBackendIDs(const std::vector<std::string>& backendStrings)
396     {
397         std::vector<BackendId> backendIDs;
398         for (const auto& b : backendStrings)
399         {
400             backendIDs.push_back(BackendId(b));
401         }
402         return backendIDs;
403     }
404 
405     /// Verifies if the program argument with the name argName contains a valid file path.
406     /// Returns the valid file path string if given argument is associated a valid file path.
407     /// Otherwise throws an exception.
GetPathArgumentParseArgs408     std::string GetPathArgument(cxxopts::ParseResult& result,
409                                 std::string&& argName,
410                                 ExpectFile expectFile,
411                                 OptionalArg isOptionalArg)
412     {
413         if (result.count(argName))
414         {
415             std::string path = result[argName].as<std::string>();
416             if (!ValidateFilePath(path, expectFile))
417             {
418                 std::stringstream ss;
419                 ss << "Argument given to" << argName << "is not a valid file path";
420                 throw cxxopts::option_syntax_exception(ss.str().c_str());
421             }
422             return path;
423         }
424         else
425         {
426             if (isOptionalArg == OptionalArg::True)
427             {
428                 return "";
429             }
430 
431             throw cxxopts::missing_argument_exception(argName);
432         }
433     }
434 
435     /// Assigns vector of strings to struct member variable
GetPathArgumentParseArgs436     std::vector<std::string> GetPathArgument(const std::vector<std::string>& pathStrings, OptionalArg isOptional)
437     {
438         if (pathStrings.size() < 5){
439             if (isOptional == OptionalArg::True)
440             {
441                 return std::vector<std::string>();
442             }
443             throw cxxopts::option_syntax_exception("Comparison files requires 5 file paths.");
444         }
445 
446         std::vector<std::string> filePaths;
447         for (auto& path : pathStrings)
448         {
449             filePaths.push_back(path);
450             if (!ValidateFilePath(filePaths.back(), ExpectFile::True))
451             {
452                 throw cxxopts::option_syntax_exception("Argument given to Comparison Files is not a valid file path");
453             }
454         }
455         return filePaths;
456     }
457 
458     /// Log info about assigned backends
LogBackendsInfoParseArgs459     void LogBackendsInfo(std::vector<BackendId>& backends, std::string&& modelName)
460     {
461         std::string info;
462         info = "Preferred backends for " + modelName + " set to [ ";
463         for (auto const &backend : backends)
464         {
465             info = info + std::string(backend) + " ";
466         }
467         ARMNN_LOG(info) << info << "]";
468     }
469 
470     // Member variables
471     std::string backboneDir;
472     std::vector<std::string> comparisonFiles;
473     std::string detectorDir;
474     std::string imageDir;
475     std::string dynamicBackendPath;
476 
477     std::vector<BackendId> prefBackendsBackbone;
478     std::vector<BackendId> prefBackendsDetector;
479 
480     cxxopts::Options options;
481 
482     DumpToDot dumpToDot;
483 };
484 
main(int argc,char * argv[])485 int main(int argc, char* argv[])
486 {
487     // Configure logging
488     SetAllLoggingSinks(true, true, true);
489     SetLogFilter(LogSeverity::Trace);
490 
491     // Check and get given program arguments
492     ParseArgs progArgs = ParseArgs(argc, argv);
493 
494     // Create runtime
495     IRuntime::CreationOptions runtimeOptions; // default
496 
497     if (!progArgs.dynamicBackendPath.empty())
498     {
499         std::cout << "Loading backends from" << progArgs.dynamicBackendPath << "\n";
500         runtimeOptions.m_DynamicBackendsPath = progArgs.dynamicBackendPath;
501     }
502 
503     auto runtime = IRuntime::Create(runtimeOptions);
504     if (!runtime)
505     {
506         ARMNN_LOG(fatal) << "Could not create runtime.";
507         return -1;
508     }
509 
510     // Create TfLite Parsers
511     ITfLiteParser::TfLiteParserOptions parserOptions;
512     auto parser = ITfLiteParser::Create(parserOptions);
513 
514     // Load backbone model
515     ARMNN_LOG(info) << "Loading backbone...";
516     NetworkId backboneId;
517     const DumpToDot dumpToDot = progArgs.dumpToDot;
518     CHECK_OK(LoadModel(progArgs.backboneDir.c_str(),
519                        *parser,
520                        *runtime,
521                        backboneId,
522                        progArgs.prefBackendsBackbone,
523                        ImportMemory::False,
524                        dumpToDot));
525     auto inputId = parser->GetNetworkInputBindingInfo(0, "inputs");
526     auto bbOut0Id = parser->GetNetworkOutputBindingInfo(0, "input_to_detector_1");
527     auto bbOut1Id = parser->GetNetworkOutputBindingInfo(0, "input_to_detector_2");
528     auto bbOut2Id = parser->GetNetworkOutputBindingInfo(0, "input_to_detector_3");
529     auto backboneProfile = runtime->GetProfiler(backboneId);
530     backboneProfile->EnableProfiling(true);
531 
532 
533     // Load detector model
534     ARMNN_LOG(info) << "Loading detector...";
535     NetworkId detectorId;
536     CHECK_OK(LoadModel(progArgs.detectorDir.c_str(),
537                        *parser,
538                        *runtime,
539                        detectorId,
540                        progArgs.prefBackendsDetector,
541                        ImportMemory::True,
542                        dumpToDot));
543     auto detectIn0Id = parser->GetNetworkInputBindingInfo(0, "input_to_detector_1");
544     auto detectIn1Id = parser->GetNetworkInputBindingInfo(0, "input_to_detector_2");
545     auto detectIn2Id = parser->GetNetworkInputBindingInfo(0, "input_to_detector_3");
546     auto outputBoxesId = parser->GetNetworkOutputBindingInfo(0, "output_boxes");
547     auto detectorProfile = runtime->GetProfiler(detectorId);
548 
549     // Load input from file
550     ARMNN_LOG(info) << "Loading test image...";
551     auto image = LoadImage(progArgs.imageDir.c_str());
552     if (image.empty())
553     {
554         return LOAD_IMAGE_ERROR;
555     }
556 
557     // Allocate the intermediate tensors
558     std::vector<float> intermediateMem0(bbOut0Id.second.GetNumElements());
559     std::vector<float> intermediateMem1(bbOut1Id.second.GetNumElements());
560     std::vector<float> intermediateMem2(bbOut2Id.second.GetNumElements());
561     std::vector<float> intermediateMem3(outputBoxesId.second.GetNumElements());
562 
563     // Setup inputs and outputs
564     using BindingInfos = std::vector<armnn::BindingPointInfo>;
565     using FloatTensors = std::vector<std::reference_wrapper<std::vector<float>>>;
566 
567     InputTensors bbInputTensors = MakeInputTensors(BindingInfos{ inputId },
568                                                    FloatTensors{ image });
569     OutputTensors bbOutputTensors = MakeOutputTensors(BindingInfos{ bbOut0Id, bbOut1Id, bbOut2Id },
570                                                       FloatTensors{ intermediateMem0,
571                                                                     intermediateMem1,
572                                                                     intermediateMem2 });
573     InputTensors detectInputTensors = MakeInputTensors(BindingInfos{ detectIn0Id,
574                                                                      detectIn1Id,
575                                                                      detectIn2Id } ,
576                                                        FloatTensors{ intermediateMem0,
577                                                                      intermediateMem1,
578                                                                      intermediateMem2 });
579     OutputTensors detectOutputTensors = MakeOutputTensors(BindingInfos{ outputBoxesId },
580                                                           FloatTensors{ intermediateMem3 });
581 
582     static const int numIterations=2;
583     using DurationUS = std::chrono::duration<double, std::micro>;
584     std::vector<DurationUS> nmsDurations(0);
585     std::vector<yolov3::Detection> filtered_boxes;
586     nmsDurations.reserve(numIterations);
587     for (int i=0; i < numIterations; i++)
588     {
589         // Execute backbone
590         ARMNN_LOG(info) << "Running backbone...";
591         runtime->EnqueueWorkload(backboneId, bbInputTensors, bbOutputTensors);
592 
593         // Execute detector
594         ARMNN_LOG(info) << "Running detector...";
595         runtime->EnqueueWorkload(detectorId, detectInputTensors, detectOutputTensors);
596 
597         // Execute NMS
598         ARMNN_LOG(info) << "Running nms...";
599         using clock = std::chrono::steady_clock;
600         auto nmsStartTime = clock::now();
601         yolov3::NMSConfig config;
602         config.num_boxes = 127800;
603         config.num_classes = 80;
604         config.confidence_threshold = 0.9f;
605         config.iou_threshold = 0.5f;
606         filtered_boxes = yolov3::nms(config, intermediateMem3);
607         auto nmsEndTime = clock::now();
608 
609         // Enable the profiling after the warm-up run
610         if (i>0)
611         {
612             print_detection(std::cout, filtered_boxes);
613 
614             const auto nmsDuration = DurationUS(nmsStartTime - nmsEndTime);
615             nmsDurations.push_back(nmsDuration);
616         }
617         backboneProfile->EnableProfiling(true);
618         detectorProfile->EnableProfiling(true);
619     }
620     // Log timings to file
621     std::ofstream backboneProfileStream("backbone.json");
622     backboneProfile->Print(backboneProfileStream);
623     backboneProfileStream.close();
624 
625     std::ofstream detectorProfileStream("detector.json");
626     detectorProfile->Print(detectorProfileStream);
627     detectorProfileStream.close();
628 
629     // Manually construct the json output
630     std::ofstream nmsProfileStream("nms.json");
631     nmsProfileStream << "{" << "\n";
632     nmsProfileStream << R"(  "NmsTimings": {)" << "\n";
633     nmsProfileStream << R"(    "raw": [)" << "\n";
634     bool isFirst = true;
635     for (auto duration : nmsDurations)
636     {
637         if (!isFirst)
638         {
639             nmsProfileStream << ",\n";
640         }
641 
642         nmsProfileStream << "      " << duration.count();
643         isFirst = false;
644     }
645     nmsProfileStream << "\n";
646     nmsProfileStream << R"(    "units": "us")" << "\n";
647     nmsProfileStream << "    ]" << "\n";
648     nmsProfileStream << "  }" << "\n";
649     nmsProfileStream << "}" << "\n";
650     nmsProfileStream.close();
651 
652     if (progArgs.comparisonFiles.size() > 0)
653     {
654         CheckAccuracy(&intermediateMem0,
655                       &intermediateMem1,
656                       &intermediateMem2,
657                       &intermediateMem3,
658                       filtered_boxes,
659                       progArgs.comparisonFiles);
660     }
661 
662     ARMNN_LOG(info) << "Run completed";
663     return 0;
664 }
665