Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding a method to read the model file from an Android Archive #333

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
8 changes: 8 additions & 0 deletions NFIQ2/NFIQ2Algorithm/include/nfiq2_algorithm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
#include <string>
#include <unordered_map>

#ifdef __ANDROID__
#include <android/asset_manager.h>
#endif

namespace NFIQ2 {

/**
Expand Down Expand Up @@ -48,6 +52,10 @@ class Algorithm {
* The md5 checksum of the provided file.
*/
Algorithm(const std::string &fileName, const std::string &fileHash);
#ifdef __ANDROID__
Algorithm(AAssetManager *assets, const std::string &fileName,
gfiumara marked this conversation as resolved.
Show resolved Hide resolved
const std::string &fileHash);
#endif

/**
* @brief
Expand Down
11 changes: 11 additions & 0 deletions NFIQ2/NFIQ2Algorithm/include/prediction/RandomForestML.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
#include <unordered_map>
#include <vector>

#ifdef __ANDROID__
#include <android/asset_manager.h>
#endif

namespace NFIQ2 { namespace Prediction {

/**
Expand Down Expand Up @@ -35,6 +39,13 @@ class RandomForestML {
std::string initModule(const std::string &fileName,
const std::string &fileHash);

#ifdef __ANDROID__
/** Initialize model from Android AAR (When not using embedded
* parameters). */
std::string initModule(AAssetManager *assets,
const std::string &fileName, const std::string &fileHash);
#endif

/**
* Compute NFIQ2 quality score based on model and provided
* QualityFeatureData.
Expand Down
8 changes: 8 additions & 0 deletions NFIQ2/NFIQ2Algorithm/src/nfiq2/nfiq2_algorithm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ NFIQ2::Algorithm::Algorithm(const NFIQ2::ModelInfo &modelInfoObj)
{
}

#ifdef __ANDROID__
NFIQ2::Algorithm::Algorithm(AAssetManager *assets, const std::string &fileName,
const std::string &fileHash)
: pimpl { new NFIQ2::Algorithm::Impl(assets, fileName, fileHash) }
{
}
#endif

NFIQ2::Algorithm::Algorithm(const Algorithm &rhs)
: pimpl(new Impl(*rhs.pimpl))
{
Expand Down
28 changes: 28 additions & 0 deletions NFIQ2/NFIQ2Algorithm/src/nfiq2/nfiq2_algorithm_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,34 @@ NFIQ2::Algorithm::Impl::Impl(const std::string &fileName,
}
}

#ifdef __ANDROID__
NFIQ2::Algorithm::Impl::Impl(AAssetManager *assets, const std::string &fileName,
const std::string &fileHash)
: initialized { false }
{
// init RF module that takes some time to load the parameters
try {
this->m_parameterHash = m_RandomForestML.initModule(assets,
fileName, fileHash);
this->initialized = true;
} catch (const cv::Exception &e) {
throw Exception(NFIQ2::ErrorCode::BadArguments,
"Could not initialize random forest parameters with "
"external file. Most likely, the file does not exist. "
"Check the path (" +
fileName + ") and hash (" + fileHash +
") (initial error: " + e.msg + ").");
} catch (const NFIQ2::Exception &e) {
throw Exception(NFIQ2::ErrorCode::BadArguments,
"Could not initialize random forest parameters with "
"external file. Most likely, the hash is not correct. "
"Check the path (" +
fileName + ") and hash (" + fileHash +
") (initial error: " + e.what() + ").");
}
}
#endif

NFIQ2::Algorithm::Impl::~Impl() = default;

double
Expand Down
16 changes: 16 additions & 0 deletions NFIQ2/NFIQ2Algorithm/src/nfiq2/nfiq2_algorithm_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ class Algorithm::Impl {
*/
Impl(const std::string &fileName, const std::string &fileHash);

#ifdef __ANDROID__
/**
* @brief
* Constructor that loads random forest parameters from AAR.
*
* @param assets
* The Android Asset Manager, provided by the App.
* @param fileName
* The file path containing the random forest model.
* @param fileHash
* The md5 checksum of the provided file.
*/
Impl(AAssetManager *assets, const std::string &fileName,
const std::string &fileHash);
#endif

/** Destructor. */
virtual ~Impl();

Expand Down
57 changes: 57 additions & 0 deletions NFIQ2/NFIQ2Algorithm/src/prediction/RandomForestML.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,63 @@ NFIQ2::Prediction::RandomForestML::initModule(const std::string &fileName,
return hash;
}

#ifdef __ANDROID__
std::string
NFIQ2::Prediction::RandomForestML::initModule(AAssetManager *assets,
const std::string &fileName, const std::string &fileHash)
{
if (assets == nullptr) {
throw NFIQ2::Exception(NFIQ2::ErrorCode::BadArguments,
"Prediction::RandomForestML::initModule: AAssetManager is null");
}
if (fileName.empty()) {
throw NFIQ2::Exception(NFIQ2::ErrorCode::BadArguments,
"Prediction::RandomForestML::initModule: "
"file name is empty");
}
if (fileHash.empty()) {
throw NFIQ2::Exception(NFIQ2::ErrorCode::BadArguments,
"Prediction::RandomForestML::initModule: "
"hash value is empty");
}
AAsset *asset = AAssetManager_open(assets, fileName.c_str(),
AASSET_MODE_STREAMING);
gfiumara marked this conversation as resolved.
Show resolved Hide resolved
if (asset == nullptr) {
throw NFIQ2::Exception(NFIQ2::ErrorCode::InvalidConfiguration,
"Prediction::RandomForestML::initModule: "
"requested asset not found");
}
// reading the file with the asset manager
std::stringstream ss;
char buffer[BUFSIZ + 1]; // BUFSIZ defined in stdio.h
int cnt = AAsset_read(asset, buffer, BUFSIZ);
gfiumara marked this conversation as resolved.
Show resolved Hide resolved
while (cnt > 0 && cnt <= BUFSIZ) {
buffer[cnt] = 0; // add terminating '\0'
ss << buffer;
Comment on lines +157 to +158
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we guaranteed to not have a null-terminator anywhere in the model file? Maybe for the current model only? Should we write instead (if we keep this loop)?

Suggested change
buffer[cnt] = 0; // add terminating '\0'
ss << buffer;
ss.write(buffer, cnt);

...or even store to a std::vector<char> instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We read the forest params as string and the yaml file with the model is a text file. Using the '\0' is safe due it is the text termination and will not occur in the text file.
I checked the reading routine in Android and

  • it does not need much time
  • the MD5 of the file content can successfully compared with the given hash string.
    Moving this to a vector or messing with the string termination will easily invalidate the hash value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can have \0 in a text file and in the middle of a string. We might not in the current model, but using operator<< (vs write with a size) will not read all BUFSIZ bytes if a future model did have a \0 inside. My overall question for this is and the two comments for line 154 are why are we reading in chunks and manipulating C-style strings if we don't have to with the API Android gives us?

Changing the storage container won't change the contents and we need to get to a C-style string for the digest function anyway (vector<char>.data()). I know calculateHashString takes a string, but it could just as easily and maybe more appropriately take something else in a refactor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RLessmann: please ignore my second comment (we have to get to a std::string to pass to OpenCV cv::FileStorage eventually, so the vector solution would duplicate RAM. However, I still find the ss.write to be more appropriate than operator<<. This should not change anything, avoids string conversion on every iteration, and guards us in the future if there is ever a \0 in a file. Would you please make and test this change on device? It should be identical. I believe you can commit the suggestion below and run the branch.

Comment on lines +154 to +158
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
char buffer[BUFSIZ + 1]; // BUFSIZ defined in stdio.h
int cnt = AAsset_read(asset, buffer, BUFSIZ);
while (cnt > 0 && cnt <= BUFSIZ) {
buffer[cnt] = 0; // add terminating '\0'
ss << buffer;
char buffer[BUFSIZ];
int cnt = AAsset_read(asset, buffer, BUFSIZ);
while (cnt > 0 && cnt <= BUFSIZ) {
ss.write(buffer, cnt);

cnt = AAsset_read(asset, buffer, BUFSIZ);
}
AAsset_close(asset);
// verify the read data and initialize model
std::string params = ss.str();
if (params.size() == 0) {
throw NFIQ2::Exception(NFIQ2::ErrorCode::InvalidConfiguration,
"The trained network could not be initialized!"
"Invalid model");
}
// calculate and compare the hash
std::string hash = calculateHashString(params);
if (fileHash.compare(hash) != 0) {
m_pTrainedRF->clear();
throw NFIQ2::Exception(NFIQ2::ErrorCode::InvalidConfiguration,
"The trained network could not be initialized! "
"Error: " +
hash);
gfiumara marked this conversation as resolved.
Show resolved Hide resolved
}
initModule(params);
return hash;
}
#endif

void
NFIQ2::Prediction::RandomForestML::evaluate(
const std::unordered_map<std::string, double> &features,
Expand Down
1 change: 1 addition & 0 deletions NFIQ2/NFIQ2Api/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ elseif("${TARGET_PLATFORM}" MATCHES "android*")
find_package(Threads REQUIRED)
set( PROJECT_LIBS ${PROJECT_LIBS}
${CMAKE_THREAD_LIBS_INIT}
android
log
)
else()
Expand Down