Files
NANOKVM-MaixCDK-MIRROR/components/nn/include/maix_nn_self_learn_classifier.hpp
2025-08-25 15:33:16 +08:00

611 lines
21 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Self learn classifier, learn anything and recognize.
* @author neucrack@sipeed
* @license Apache 2.0
* @date 2024.6.14 Add support.
*/
#pragma once
#include "maix_basic.hpp"
#include "maix_nn.hpp"
namespace maix::nn
{
/**
* SelfLearnClassifier
* @maixpy maix.nn.SelfLearnClassifier
*/
class SelfLearnClassifier
{
public:
/**
* Construct a new SelfLearnClassifier object
* @param model MUD model path, if empty, will not load model, you can call load_model() later.
* if not empty, will load model and will raise err::Exception if load failed.
* @param[in] dual_buff prepare dual input output buffer to accelarate forward, that is, when NPU is forwarding we not wait and prepare the next input buff.
* If you want to ensure every time forward output the input's result, set this arg to false please.
* Default true to ensure speed.
* @maixpy maix.nn.SelfLearnClassifier.__init__
* @maixcdk maix.nn.SelfLearnClassifier.SelfLearnClassifier
*/
SelfLearnClassifier(const std::string &model = "", bool dual_buff = true)
{
_model = nullptr;
_feature_num = 0;
_dual_buff = dual_buff;
if (!model.empty())
{
err::Err e = load_model(model);
if (e != err::ERR_NONE)
{
throw err::Exception(e, "load model failed");
}
}
}
~SelfLearnClassifier()
{
if (_model)
{
delete _model;
_model = nullptr;
}
for (auto i : _features)
{
delete[] i;
}
for (auto i : _features_sample)
{
delete[] i;
}
}
/**
* Load model from file, model format is .mud,
* MUD file should contain [extra] section, have key-values:
* - model_type: classifier_no_top
* - input_type: rgb or bgr
* - mean: 123.675, 116.28, 103.53
* - scale: 0.017124753831663668, 0.01750700280112045, 0.017429193899782137
* @param model MUD model path
* @return error code, if load failed, return error code
* @maixpy maix.nn.SelfLearnClassifier.load_model
*/
err::Err load_model(const string &model)
{
if (_model)
{
delete _model;
_model = nullptr;
}
_model = new nn::NN(model, _dual_buff);
if (!_model)
{
return err::ERR_NO_MEM;
}
_extra_info = _model->extra_info();
if (_extra_info.find("model_type") != _extra_info.end())
{
if (_extra_info["model_type"] != "classifier_no_top")
{
log::error("model_type not match, expect 'classifier_no_top', but got '%s'", _extra_info["model_type"].c_str());
return err::ERR_ARGS;
}
}
else
{
log::error("model_type key not found");
return err::ERR_ARGS;
}
if (_extra_info.find("input_type") != _extra_info.end())
{
std::string input_type = _extra_info["input_type"];
if (input_type == "rgb")
{
_input_img_fmt = maix::image::FMT_RGB888;
}
else if (input_type == "bgr")
{
_input_img_fmt = maix::image::FMT_BGR888;
}
else
{
log::error("unknown input type: %s", input_type.c_str());
return err::ERR_ARGS;
}
_input_is_img = true;
}
else
{
log::error("input_type key not found");
return err::ERR_ARGS;
}
if (_extra_info.find("mean") != _extra_info.end())
{
std::string mean_str = _extra_info["mean"];
std::vector<std::string> mean_strs = split(mean_str, ",");
for (auto &it : mean_strs)
{
try
{
this->mean.push_back(std::stof(it));
}
catch (std::exception &e)
{
log::error("mean value error, should float");
return err::ERR_ARGS;
}
}
}
else
{
log::error("mean key not found");
return err::ERR_ARGS;
}
if (_extra_info.find("scale") != _extra_info.end())
{
std::string scale_str = _extra_info["scale"];
std::vector<std::string> scale_strs = split(scale_str, ",");
for (auto &it : scale_strs)
{
try
{
this->scale.push_back(std::stof(it));
}
catch (std::exception &e)
{
log::error("scale value error, should float");
return err::ERR_ARGS;
}
}
}
else
{
log::error("scale key not found");
return err::ERR_ARGS;
}
_inputs = _model->inputs_info();
if(_inputs[0].shape[1] <= 4)
_input_size = image::Size(_inputs[0].shape[3], _inputs[0].shape[2]);
else
_input_size = image::Size(_inputs[0].shape[2], _inputs[0].shape[1]);
_feature_num = _model->outputs_info()[0].shape_int();
log::info("feature num: %d", _feature_num);
return err::ERR_NONE;
}
/**
* Classify image
* @param img image, format should match model input_type or will raise err.Exception
* @param fit image resize fit mode, default Fit.FIT_COVER, see image.Fit.
* @throw If error occurred, will raise err::Exception, you can find reason in log, mostly caused by args error or hardware error.
* @return result, a list of (idx, distance), smaller distance means more similar. In C++, you need to delete it after use.
* @maixpy maix.nn.SelfLearnClassifier.classify
*/
std::vector<std::pair<int, float>> *classify(image::Image &img, image::Fit fit = image::FIT_COVER)
{
float *feature = NULL;
tensor::Tensors *outs = _get_feature(img, &feature, fit);
std::vector<std::pair<int, float>> *distances = new std::vector<std::pair<int, float>>();
for (size_t i = 0; i < _features.size(); ++i)
{
float distance = _get_distance(feature, _features[i]);
distances->push_back(std::make_pair(i, distance));
}
delete outs;
// sort
std::sort(distances->begin(), distances->end(), [](const std::pair<int, float> &a, const std::pair<int, float> &b)
{ return a.second < b.second; });
return distances;
}
/**
* Add a class to recognize
* @param img Add a image as a new class
* @param fit image resize fit mode, default Fit.FIT_COVER, see image.Fit.
* @maixpy maix.nn.SelfLearnClassifier.add_class
*/
void add_class(image::Image &img, image::Fit fit = image::FIT_COVER)
{
float *feature = NULL;
tensor::Tensors *outs = _get_feature(img, &feature, fit);
_add_feature(feature);
delete outs;
}
/**
* Get class number
* @maixpy maix.nn.SelfLearnClassifier.class_num
*/
int class_num()
{
return (int)_features.size();
}
/**
* Remove a class
* @param idx index, value from 0 to class_num();
* @maixpy maix.nn.SelfLearnClassifier.rm_class
*/
err::Err rm_class(int idx)
{
if ((size_t)idx >= _features.size())
return err::ERR_ARGS;
delete[] _features[idx];
_features.erase(_features.begin() + idx);
return err::ERR_NONE;
}
/**
* Add sample, you should call learn method after add some samples to learn classes.
* Sample image can be any of classes we already added.
* @param img Add a image as a new sample.
* @maixpy maix.nn.SelfLearnClassifier.add_sample
*/
void add_sample(image::Image &img, image::Fit fit = image::FIT_COVER)
{
float *feature = NULL;
tensor::Tensors *outs = _get_feature(img, &feature, fit);
_add_feature_sample(feature);
delete outs;
}
/**
* Remove a sample
* @param idx index, value from 0 to sample_num();
* @maixpy maix.nn.SelfLearnClassifier.rm_sample
*/
err::Err rm_sample(int idx)
{
if ((size_t)idx >= _features_sample.size())
return err::ERR_ARGS;
delete[] _features_sample[idx];
_features_sample.erase(_features_sample.begin() + idx);
return err::ERR_NONE;
}
/**
* Get sample number
* @maixpy maix.nn.SelfLearnClassifier.sample_num
*/
int sample_num()
{
return (int)_features_sample.size();
}
/**
* Start auto learn class features from classes image and samples.
* You should call this method after you add some samples.
* @return learn epoch(times), 0 means learn nothing.
* @maixpy maix.nn.SelfLearnClassifier.learn
*/
int learn();
/**
* Clear all class and samples
* @maixpy maix.nn.SelfLearnClassifier.clear
*/
void clear()
{
for (auto i : _features)
{
delete[] i;
}
_features.clear();
for (auto i : _features_sample)
{
delete[] i;
}
_features_sample.clear();
}
/**
* Get model input size, only for image input
* @return model input size
* @maixpy maix.nn.SelfLearnClassifier.input_size
*/
image::Size input_size()
{
return _input_size;
}
/**
* Get model input width, only for image input
* @return model input size of width
* @maixpy maix.nn.SelfLearnClassifier.input_width
*/
int input_width()
{
return _input_size.width();
}
/**
* Get model input height, only for image input
* @return model input size of height
* @maixpy maix.nn.SelfLearnClassifier.input_height
*/
int input_height()
{
return _input_size.height();
}
/**
* Get input image format, only for image input
* @return input image format, image::Format type.
* @maixpy maix.nn.SelfLearnClassifier.input_format
*/
image::Format input_format()
{
return _input_img_fmt;
}
/**
* Get input shape, if have multiple input, only return first input shape
* @return input shape, list type
* @maixpy maix.nn.SelfLearnClassifier.input_shape
*/
std::vector<int> input_shape()
{
return _inputs[0].shape;
}
/**
* Save features and labels to a binary file
* @param path file path to save, e.g. /root/my_classes.bin
* @param labels class labels, can be None, or length must equal to class num, or will return err::Err
* @return maix.err.Err if labels exists but length not equal to class num, or save file failed, or class num is 0.
* @maixpy maix.nn.SelfLearnClassifier.save
*/
err::Err save(const std::string &path, const std::vector<std::string> &labels = std::vector<std::string>())
{
// 1B: version, now only 0
// 4B: (n) class num, int32_t type
// 4B: (m) sample num, int32_t type
// 4B: (f) feature length, int32_t type
// 1B: have labels, uint8_t type, 0 mean no, 1 means have
// *B: labels(if have), every label ends with \0
// n*fB: n(class num) class features, every feature length is f.
// m*fB: m(class num) sample features, every feature length is f.
// std::vector<float *> _features;
// std::vector<float *> _features_sample;
if (_features.empty())
{
log::error("class num must > 0");
return maix::err::ERR_ARGS;
}
// Check if labels size matches the number of classes
if (!labels.empty() && labels.size() != _features.size())
{
log::error("labels length must equal to class num");
return maix::err::ERR_ARGS;
}
fs::File *f = maix::fs::open(path, "wb");
if (!f)
{
log::error("Failed to open file for saving");
return maix::err::ERR_IO;
}
uint8_t version = 0;
int32_t class_num = static_cast<int32_t>(_features.size());
int32_t sample_num = static_cast<int32_t>(_features_sample.size());
int32_t feature_length = static_cast<int32_t>(_feature_num);
uint8_t have_labels = labels.empty() ? 0 : 1;
f->write(&version, sizeof(version));
f->write(&class_num, sizeof(class_num));
f->write(&sample_num, sizeof(sample_num));
f->write(&feature_length, sizeof(feature_length));
f->write(&have_labels, sizeof(have_labels));
if (have_labels)
{
for (const auto &label : labels)
{
f->write(label.c_str(), label.size() + 1);
}
}
for (const auto &feature : _features)
{
f->write(reinterpret_cast<const char *>(feature), _feature_num * sizeof(float));
}
for (const auto &feature : _features_sample)
{
f->write(reinterpret_cast<const char *>(feature), _feature_num * sizeof(float));
}
f->close();
delete f; // Make sure to delete the file object to avoid memory leaks
return maix::err::ERR_NONE;
}
/**
* Load features info from binary file
* @param path feature info binary file path, e.g. /root/my_classes.bin
* @maixpy maix.nn.SelfLearnClassifier.load
*/
std::vector<std::string> load(const std::string &path)
{
fs::File *f = maix::fs::open(path, "rb");
if (!f)
{
log::error("Open failed");
throw err::Exception(err::ERR_IO);
}
uint8_t version;
int32_t class_num, sample_num, feature_length;
uint8_t have_labels;
f->read(&version, sizeof(version));
f->read(&class_num, sizeof(class_num));
f->read(&sample_num, sizeof(sample_num));
f->read(&feature_length, sizeof(feature_length));
f->read(&have_labels, sizeof(have_labels));
if (feature_length != _feature_num)
{
log::error("feature length(%d) not equal to this model's(%d)", feature_length, _feature_num);
throw err::Exception(err::ERR_ARGS);
}
std::vector<std::string> labels;
if (have_labels)
{
for (int i = 0; i < class_num; ++i)
{
std::string label;
char c;
while (f->read(&c, 1) == 1 && c != '\0')
{
label += c;
}
labels.push_back(label);
}
}
for (auto i : _features)
{
delete[] i;
}
_features.clear();
for (int i = 0; i < class_num; ++i)
{
float *feature = new float[_feature_num];
_features.push_back(feature);
f->read(reinterpret_cast<char *>(feature), feature_length * sizeof(float));
}
for (auto i : _features_sample)
{
delete[] i;
}
_features_sample.clear();
for (int i = 0; i < sample_num; ++i)
{
float *feature = new float[_feature_num];
_features_sample.push_back(feature);
f->read(reinterpret_cast<char *>(feature), feature_length * sizeof(float));
}
f->close();
delete f; // Make sure to delete the file object to avoid memory leaks
return labels;
}
public:
/**
* Labels list
* @maixpy maix.nn.SelfLearnClassifier.labels
*/
std::vector<string> labels;
/**
* Label file path
* @maixpy maix.nn.SelfLearnClassifier.label_path
*/
std::string label_path;
/**
* Get mean value, list type
* @maixpy maix.nn.SelfLearnClassifier.mean
*/
std::vector<float> mean;
/**
* Get scale value, list type
* @maixpy maix.nn.SelfLearnClassifier.scale
*/
std::vector<float> scale;
private:
image::Format _input_img_fmt;
bool _input_is_img;
nn::NN *_model;
std::map<string, string> _extra_info;
image::Size _input_size;
int _feature_num;
bool _dual_buff;
std::vector<nn::LayerInfo> _inputs;
std::vector<float *> _features;
std::vector<float *> _features_sample;
static void split0(std::vector<std::string> &items, const std::string &s, const std::string &delimiter)
{
items.clear();
size_t pos_start = 0, pos_end, delim_len = delimiter.length();
std::string token;
while ((pos_end = s.find(delimiter, pos_start)) != std::string::npos)
{
token = s.substr(pos_start, pos_end - pos_start);
pos_start = pos_end + delim_len;
items.push_back(token);
}
items.push_back(s.substr(pos_start));
}
static std::vector<std::string> split(const std::string &s, const std::string &delimiter)
{
std::vector<std::string> tokens;
split0(tokens, s, delimiter);
return tokens;
}
/**
* You should detele result after use features.
*/
tensor::Tensors *_get_feature(image::Image &img, float **feature, image::Fit fit = image::FIT_COVER)
{
if (img.format() != _input_img_fmt)
{
throw err::Exception("image format not match, input_type: " + image::fmt_names[_input_img_fmt] + ", image format: " + image::fmt_names[img.format()]);
}
tensor::Tensors *outputs = _model->forward_image(img, this->mean, this->scale, fit, false, true);
if (!outputs) // can be here!
{
throw err::Exception("forward image failed");
}
tensor::Tensor *tensor = outputs->begin()->second;
if (tensor->dtype() != tensor::DType::FLOAT32)
{
throw err::Exception("output tensor dtype only support float32 now");
}
*feature = (float *)tensor->data();
return outputs;
}
void _add_feature(float *new_feature)
{
float *feature = new float[_feature_num];
memcpy(feature, new_feature, _feature_num * sizeof(float));
_features.push_back(feature);
}
void _add_feature_sample(float *new_feature)
{
float *feature = new float[_feature_num];
memcpy(feature, new_feature, _feature_num * sizeof(float));
_features_sample.push_back(feature);
}
float _get_distance(float *a, float *b)
{
float sum = 0.;
for (int i = 0; i < _feature_num; i++)
{
sum += (a[i] - b[i]) * (a[i] - b[i]);
}
sum = sqrtf(sum);
return sum;
}
}; // class SelfLearnClassifier
} // namespace maix::nn