/* * FindObject.cpp * * Created on: 2014-07-30 * Author: mathieu */ #include "FindObject.h" #include "ObjSignature.h" #include "Settings.h" #include "utilite/UDirectory.h" #include "utilite/ULogger.h" #include "Vocabulary.h" #include #include #include #include #include FindObject::FindObject(QObject * parent) : QObject(parent), vocabulary_(new Vocabulary()), detector_(Settings::createKeypointDetector()), extractor_(Settings::createDescriptorExtractor()), minMatchedDistance_(-1), maxMatchedDistance_(-1) { Q_ASSERT(detector_ != 0 && extractor_ != 0); } FindObject::~FindObject() { delete detector_; delete extractor_; delete vocabulary_; objectsDescriptors_.clear(); } int FindObject::loadObjects(const QString & dirPath) { int loadedObjects = 0; QString formats = Settings::getGeneral_imageFormats().remove('*').remove('.'); UDirectory dir(dirPath.toStdString(), formats.toStdString()); if(dir.isValid()) { const std::list & names = dir.getFileNames(); // sorted in natural order for(std::list::const_iterator iter=names.begin(); iter!=names.end(); ++iter) { this->addObject((dirPath.toStdString()+dir.separator()+*iter).c_str()); } if(names.size()) { this->updateObjects(); this->updateVocabulary(); } loadedObjects = (int)names.size(); } return loadedObjects; } const ObjSignature * FindObject::addObject(const QString & filePath) { UINFO("Load file %s", filePath.toStdString().c_str()); if(!filePath.isNull()) { cv::Mat img = cv::imread(filePath.toStdString().c_str(), cv::IMREAD_GRAYSCALE); if(!img.empty()) { int id = 0; QFileInfo file(filePath); QStringList list = file.fileName().split('.'); if(list.size()) { bool ok = false; id = list.front().toInt(&ok); if(ok && id>0) { if(objects_.contains(id)) { UWARN("Object %d already added, a new ID will be generated (new id=%d).", id, Settings::getGeneral_nextObjID()); id = 0; } } else { id = 0; } } return this->addObject(img, id); } } return 0; } const ObjSignature * FindObject::addObject(const cv::Mat & image, int id) { Q_ASSERT(id >= 0); ObjSignature * s = new ObjSignature(id, image); if(!this->addObject(s)) { delete s; return 0; } return s; } bool FindObject::addObject(ObjSignature * obj) { Q_ASSERT(obj != 0 && obj->id() >= 0); if(obj->id() && objects_.contains(obj->id())) { UERROR("object with id %d already added!", obj->id()); return false; } else if(obj->id() == 0) { obj->setId(Settings::getGeneral_nextObjID()); } Settings::setGeneral_nextObjID(obj->id()+1); objects_.insert(obj->id(), obj); clearVocabulary(); return true; } void FindObject::removeObject(int id) { if(objects_.contains(id)) { delete objects_.value(id); objects_.remove(id); clearVocabulary(); } } void FindObject::removeAllObjects() { qDeleteAll(objects_); objects_.clear(); clearVocabulary(); } void FindObject::updateDetectorExtractor() { delete detector_; delete extractor_; detector_ = Settings::createKeypointDetector(); extractor_ = Settings::createDescriptorExtractor(); Q_ASSERT(detector_ != 0 && extractor_ != 0); } std::vector limitKeypoints(const std::vector & keypoints, int maxKeypoints) { std::vector kptsKept; if(maxKeypoints > 0 && (int)keypoints.size() > maxKeypoints) { // Sort words by response std::multimap reponseMap; // for(unsigned int i = 0; i (fabs(keypoints[i].response), i)); } // Remove them std::multimap::reverse_iterator iter = reponseMap.rbegin(); kptsKept.resize(maxKeypoints); for(unsigned int k=0; k < kptsKept.size() && iter!=reponseMap.rend(); ++k, ++iter) { kptsKept[k] = keypoints[iter->second]; } } else { kptsKept = keypoints; } return kptsKept; } class ExtractFeaturesThread : public QThread { public: ExtractFeaturesThread(int objectId, const cv::Mat & image) : objectId_(objectId), image_(image) { } int objectId() const {return objectId_;} const cv::Mat & image() const {return image_;} const std::vector & keypoints() const {return keypoints_;} const cv::Mat & descriptors() const {return descriptors_;} protected: virtual void run() { QTime time; time.start(); UINFO("Extracting descriptors from object %d...", objectId_); KeypointDetector * detector = Settings::createKeypointDetector(); keypoints_.clear(); descriptors_ = cv::Mat(); detector->detect(image_, keypoints_); delete detector; if(keypoints_.size()) { int maxFeatures = Settings::getFeature2D_3MaxFeatures(); if(maxFeatures > 0 && (int)keypoints_.size() > maxFeatures) { int previousCount = (int)keypoints_.size(); keypoints_ = limitKeypoints(keypoints_, maxFeatures); UINFO("obj=%d, %d keypoints removed, (kept %d), min/max response=%f/%f", objectId_, previousCount-(int)keypoints_.size(), (int)keypoints_.size(), keypoints_.size()?keypoints_.back().response:0.0f, keypoints_.size()?keypoints_.front().response:0.0f); } DescriptorExtractor * extractor = Settings::createDescriptorExtractor(); extractor->compute(image_, keypoints_, descriptors_); delete extractor; if((int)keypoints_.size() != descriptors_.rows) { UERROR("obj=%d kpt=%d != descriptors=%d", objectId_, (int)keypoints_.size(), descriptors_.rows); } } else { UWARN("no features detected in object %d !?!", objectId_); } UINFO("%d descriptors extracted from object %d (in %d ms)", descriptors_.rows, objectId_, time.elapsed()); } private: int objectId_; cv::Mat image_; std::vector keypoints_; cv::Mat descriptors_; }; void FindObject::updateObjects() { if(objects_.size()) { int threadCounts = Settings::getGeneral_threads(); if(threadCounts == 0) { threadCounts = objects_.size(); } QTime time; time.start(); UINFO("Features extraction from %d objects...", objects_.size()); QList objectsList = objects_.values(); for(int i=0; i threads; for(int k=i; kid(), objectsList.at(k)->image())); threads.back()->start(); } for(int j=0; jwait(); int id = threads[j]->objectId(); objects_.value(id)->setData( threads[j]->keypoints(), threads[j]->descriptors(), Settings::currentDetectorType(), Settings::currentDescriptorType()); } } UINFO("Features extraction from %d objects... done! (%d ms)", objects_.size(), time.elapsed()); } else { UINFO("No objects to update..."); } } void FindObject::clearVocabulary() { objectsDescriptors_.clear(); dataRange_.clear(); vocabulary_->clear(); } void FindObject::updateVocabulary() { clearVocabulary(); int count = 0; int dim = -1; int type = -1; // Get the total size and verify descriptors QList objectsList = objects_.values(); for(int i=0; idescriptors().empty()) { if(dim >= 0 && objectsList.at(i)->descriptors().cols != dim) { UERROR("Descriptors of the objects are not all the same size! Objects " "opened must have all the same size (and from the same descriptor extractor)."); return; } dim = objectsList.at(i)->descriptors().cols; if(type >= 0 && objectsList.at(i)->descriptors().type() != type) { UERROR("Descriptors of the objects are not all the same type! Objects opened " "must have been processed by the same descriptor extractor."); return; } type = objectsList.at(i)->descriptors().type(); count += objectsList.at(i)->descriptors().rows; } } // Copy data if(count) { UINFO("Updating global descriptors matrix: Objects=%d, total descriptors=%d, dim=%d, type=%d", (int)objects_.size(), count, dim, type); if(Settings::getGeneral_invertedSearch() || Settings::getGeneral_threads() == 1) { // If only one thread, put all descriptors in the same cv::Mat objectsDescriptors_.insert(0, cv::Mat(count, dim, type)); int row = 0; for(int i=0; idescriptors().rows) { cv::Mat dest(objectsDescriptors_.begin().value(), cv::Range(row, row+objectsList.at(i)->descriptors().rows)); objectsList.at(i)->descriptors().copyTo(dest); row += objectsList.at(i)->descriptors().rows; // dataRange contains the upper_bound for each // object (the last descriptors position in the // global object descriptors matrix) if(objectsList.at(i)->descriptors().rows) { dataRange_.insert(row-1, objectsList.at(i)->id()); } } } if(Settings::getGeneral_invertedSearch()) { QTime time; time.start(); bool incremental = Settings::getGeneral_vocabularyIncremental(); if(incremental) { UINFO("Creating incremental vocabulary..."); } else { UINFO("Creating vocabulary..."); } QTime localTime; localTime.start(); int updateVocabularyMinWords = Settings::getGeneral_vocabularyUpdateMinWords(); int addedWords = 0; for(int i=0; i words = vocabulary_->addWords(objectsList[i]->descriptors(), objectsList.at(i)->id(), incremental); objectsList[i]->setWords(words); addedWords += words.uniqueKeys().size(); bool updated = false; if(incremental && addedWords && addedWords >= updateVocabularyMinWords) { vocabulary_->update(); addedWords = 0; updated = true; } UINFO("Object %d, %d words from %d descriptors (%d words, %d ms) %s", objectsList[i]->id(), words.uniqueKeys().size(), objectsList[i]->descriptors().rows, vocabulary_->size(), localTime.restart(), updated?"updated":""); } if(addedWords) { vocabulary_->update(); } if(incremental) { UINFO("Creating incremental vocabulary... done! size=%d (%d ms)", vocabulary_->size(), time.elapsed()); } else { UINFO("Creating vocabulary... done! size=%d (%d ms)", vocabulary_->size(), time.elapsed()); } } } else { for(int i=0; iid(), objectsList.at(i)->descriptors()); } } } } class SearchThread: public QThread { public: SearchThread(Vocabulary * vocabulary, int objectId, const cv::Mat * descriptors, const QMultiMap * sceneWords) : vocabulary_(vocabulary), objectId_(objectId), descriptors_(descriptors), sceneWords_(sceneWords), minMatchedDistance_(-1.0f), maxMatchedDistance_(-1.0f) { Q_ASSERT(index && descriptors); } virtual ~SearchThread() {} int getObjectId() const {return objectId_;} float getMinMatchedDistance() const {return minMatchedDistance_;} float getMaxMatchedDistance() const {return maxMatchedDistance_;} const QMultiMap & getMatches() const {return matches_;} protected: virtual void run() { //QTime time; //time.start(); cv::Mat results; cv::Mat dists; //match objects to scene int k = Settings::getNearestNeighbor_3nndrRatioUsed()?2:1; results = cv::Mat(descriptors_->rows, k, CV_32SC1); // results index dists = cv::Mat(descriptors_->rows, k, CV_32FC1); // Distance results are CV_32FC1 vocabulary_->search(*descriptors_, results, dists, k); // PROCESS RESULTS // Get all matches for each object for(int i=0; i(i,0) <= Settings::getNearestNeighbor_4nndrRatio() * dists.at(i,1)) { matched = true; } if((matched || !Settings::getNearestNeighbor_3nndrRatioUsed()) && Settings::getNearestNeighbor_5minDistanceUsed()) { if(dists.at(i,0) <= Settings::getNearestNeighbor_6minDistance()) { matched = true; } else { matched = false; } } if(!matched && !Settings::getNearestNeighbor_3nndrRatioUsed() && !Settings::getNearestNeighbor_5minDistanceUsed()) { matched = true; // no criterion, match to the nearest descriptor } if(minMatchedDistance_ == -1 || minMatchedDistance_ > dists.at(i,0)) { minMatchedDistance_ = dists.at(i,0); } if(maxMatchedDistance_ == -1 || maxMatchedDistance_ < dists.at(i,0)) { maxMatchedDistance_ = dists.at(i,0); } int wordId = results.at(i,0); if(matched && sceneWords_->count(wordId) == 1) { matches_.insert(i, sceneWords_->value(wordId)); matches_.insert(i, results.at(i,0)); } } //UINFO("Search Object %d time=%d ms", objectIndex_, time.elapsed()); } private: Vocabulary * vocabulary_; // would be const but flann search() method is not const!? int objectId_; const cv::Mat * descriptors_; const QMultiMap * sceneWords_; // float minMatchedDistance_; float maxMatchedDistance_; QMultiMap matches_; }; class HomographyThread: public QThread { public: HomographyThread( const QMultiMap * matches, // int objectId, const std::vector * kptsA, const std::vector * kptsB) : matches_(matches), objectId_(objectId), kptsA_(kptsA), kptsB_(kptsB) { Q_ASSERT(matches && kptsA && kptsB); } virtual ~HomographyThread() {} int getObjectId() const {return objectId_;} const std::vector & getIndexesA() const {return indexesA_;} const std::vector & getIndexesB() const {return indexesB_;} const std::vector & getOutlierMask() const {return outlierMask_;} QMultiMap getInliers() const {return inliers_;} QMultiMap getOutliers() const {return outliers_;} const cv::Mat & getHomography() const {return h_;} protected: virtual void run() { //QTime time; //time.start(); std::vector mpts_1(matches_->size()); std::vector mpts_2(matches_->size()); indexesA_.resize(matches_->size()); indexesB_.resize(matches_->size()); int j=0; for(QMultiMap::const_iterator iter = matches_->begin(); iter!=matches_->end(); ++iter) { mpts_1[j] = kptsA_->at(iter.key()).pt; indexesA_[j] = iter.key(); mpts_2[j] = kptsB_->at(iter.value()).pt; indexesB_[j] = iter.value(); ++j; } if((int)mpts_1.size() >= Settings::getHomography_minimumInliers()) { h_ = findHomography(mpts_1, mpts_2, Settings::getHomographyMethod(), Settings::getHomography_ransacReprojThr(), outlierMask_); for(unsigned int k=0; k * matches_; int objectId_; const std::vector * kptsA_; const std::vector * kptsB_; std::vector indexesA_; std::vector indexesB_; std::vector outlierMask_; QMultiMap inliers_; QMultiMap outliers_; cv::Mat h_; }; void FindObject::detect(const cv::Mat & image) { QMultiMap > objects; this->detect(image, objects); if(objects.size() > 0 || Settings::getGeneral_sendNoObjDetectedEvents()) { Q_EMIT objectsFound(objects); } if(objects.size() > 1) { UINFO("(%s) %d objects detected!", QTime::currentTime().toString("HH:mm:ss.zzz").toStdString().c_str(), (int)objects.size()); } else if(objects.size() == 1) { UINFO("(%s) Object %d detected!", QTime::currentTime().toString("HH:mm:ss.zzz").toStdString().c_str(), (int)objects.begin().key()); } else if(Settings::getGeneral_sendNoObjDetectedEvents()) { UINFO("(%s) No objects detected.", QTime::currentTime().toString("HH:mm:ss.zzz").toStdString().c_str()); } } bool FindObject::detect(const cv::Mat & image, QMultiMap > & objectsDetected) { QTime totalTime; totalTime.start(); // reset statistics timeStamps_.clear(); sceneKeypoints_.clear(); sceneDescriptors_ = cv::Mat(); sceneWords_.clear(); matches_.clear(); inliers_.clear(); outliers_.clear(); minMatchedDistance_ = -1.0f; maxMatchedDistance_ = -1.0f; bool success = false; if(!image.empty()) { //Convert to grayscale cv::Mat grayscaleImg; if(image.channels() != 1 || image.depth() != CV_8U) { cv::cvtColor(image, grayscaleImg, cv::COLOR_BGR2GRAY); } else { grayscaleImg = image; } QTime time; time.start(); // EXTRACT KEYPOINTS detector_->detect(grayscaleImg, sceneKeypoints_); timeStamps_.insert(kTimeKeypointDetection, time.restart()); if(sceneKeypoints_.size()) { int maxFeatures = Settings::getFeature2D_3MaxFeatures(); if(maxFeatures > 0 && (int)sceneKeypoints_.size() > maxFeatures) { sceneKeypoints_ = limitKeypoints(sceneKeypoints_, maxFeatures); } // EXTRACT DESCRIPTORS extractor_->compute(grayscaleImg, sceneKeypoints_, sceneDescriptors_); if((int)sceneKeypoints_.size() != sceneDescriptors_.rows) { UERROR("kpt=%d != descriptors=%d\n", (int)sceneKeypoints_.size(), sceneDescriptors_.rows); } } else { UWARN("no features detected !?!\n"); } timeStamps_.insert(kTimeDescriptorExtraction, time.restart()); bool consistentNNData = (vocabulary_->size()!=0 && vocabulary_->wordToObjects().begin().value()!=-1 && Settings::getGeneral_invertedSearch()) || ((vocabulary_->size()==0 || vocabulary_->wordToObjects().begin().value()==-1) && !Settings::getGeneral_invertedSearch()); // COMPARE if(!objectsDescriptors_.empty() && sceneKeypoints_.size() && consistentNNData && objectsDescriptors_.begin().value().cols == sceneDescriptors_.cols && objectsDescriptors_.begin().value().type() == sceneDescriptors_.type()) // binary descriptor issue, if the dataTree is not yet updated with modified settings { success = true; QMultiMap words; if(!Settings::getGeneral_invertedSearch()) { // CREATE INDEX for the scene vocabulary_->clear(); words = vocabulary_->addWords(sceneDescriptors_, -1, Settings::getGeneral_vocabularyIncremental()); if(!Settings::getGeneral_vocabularyIncremental()) { vocabulary_->update(); } timeStamps_.insert(kTimeIndexing, time.restart()); } for(QMap::iterator iter=objects_.begin(); iter!=objects_.end(); ++iter) { matches_.insert(iter.key(), QMultiMap()); } if(Settings::getGeneral_invertedSearch() || Settings::getGeneral_threads() == 1) { cv::Mat results; cv::Mat dists; // DO NEAREST NEIGHBOR int k = Settings::getNearestNeighbor_3nndrRatioUsed()?2:1; if(!Settings::getGeneral_invertedSearch()) { //match objects to scene results = cv::Mat(objectsDescriptors_.begin().value().rows, k, CV_32SC1); // results index dists = cv::Mat(objectsDescriptors_.begin().value().rows, k, CV_32FC1); // Distance results are CV_32FC1 vocabulary_->search(objectsDescriptors_.begin().value(), results, dists, k); } else { //match scene to objects results = cv::Mat(sceneDescriptors_.rows, k, CV_32SC1); // results index dists = cv::Mat(sceneDescriptors_.rows, k, CV_32FC1); // Distance results are CV_32FC1 vocabulary_->search(sceneDescriptors_, results, dists, k); } // PROCESS RESULTS // Get all matches for each object for(int i=0; i(i,0) <= Settings::getNearestNeighbor_4nndrRatio() * dists.at(i,1)) { matched = true; } if((matched || !Settings::getNearestNeighbor_3nndrRatioUsed()) && Settings::getNearestNeighbor_5minDistanceUsed()) { if(dists.at(i,0) <= Settings::getNearestNeighbor_6minDistance()) { matched = true; } else { matched = false; } } if(!matched && !Settings::getNearestNeighbor_3nndrRatioUsed() && !Settings::getNearestNeighbor_5minDistanceUsed()) { matched = true; // no criterion, match to the nearest descriptor } if(minMatchedDistance_ == -1 || minMatchedDistance_ > dists.at(i,0)) { minMatchedDistance_ = dists.at(i,0); } if(maxMatchedDistance_ == -1 || maxMatchedDistance_ < dists.at(i,0)) { maxMatchedDistance_ = dists.at(i,0); } if(matched) { if(Settings::getGeneral_invertedSearch()) { int wordId = results.at(i,0); QList objIds = vocabulary_->wordToObjects().values(wordId); for(int j=0; jwordToObjects().count(wordId, objIds[j]) == 1) { matches_.find(objIds[j]).value().insert(objects_.value(objIds[j])->words().value(wordId), i); } } } else { QMap::iterator iter = dataRange_.lowerBound(i); int objectId = iter.value(); int fisrtObjectDescriptorIndex = (iter == dataRange_.begin())?0:(--iter).key()+1; int objectDescriptorIndex = i - fisrtObjectDescriptorIndex; int wordId = results.at(i,0); if(words.count(wordId) == 1) { matches_.find(objectId).value().insert(objectDescriptorIndex, words.value(wordId)); } } } } } else { //multi-threaded, match objects to scene int threadCounts = Settings::getGeneral_threads(); if(threadCounts == 0) { threadCounts = (int)objectsDescriptors_.size(); } QList objectsDescriptorsId = objectsDescriptors_.keys(); QList objectsDescriptorsMat = objectsDescriptors_.values(); for(int j=0; j threads; for(int k=j; kstart(); } for(int k=0; kwait(); matches_[threads[k]->getObjectId()] = threads[k]->getMatches(); if(minMatchedDistance_ == -1 || minMatchedDistance_ > threads[k]->getMinMatchedDistance()) { minMatchedDistance_ = threads[k]->getMinMatchedDistance(); } if(maxMatchedDistance_ == -1 || maxMatchedDistance_ < threads[k]->getMaxMatchedDistance()) { maxMatchedDistance_ = threads[k]->getMaxMatchedDistance(); } delete threads[k]; } } } timeStamps_.insert(kTimeMatching, time.restart()); // Homographies if(Settings::getHomography_homographyComputed()) { // HOMOGRAPHY int threadCounts = Settings::getGeneral_threads(); if(threadCounts == 0) { threadCounts = matches_.size(); } QList matchesId = matches_.keys(); QList > matchesList = matches_.values(); for(int i=0; i threads; for(int k=i; kkeypoints(), &sceneKeypoints_)); threads.back()->start(); } for(int j=0; jwait(); int id = threads[j]->getObjectId(); if(!threads[j]->getHomography().empty()) { if(threads[j]->getInliers().size() >= Settings::getHomography_minimumInliers()) { const cv::Mat & H = threads[j]->getHomography(); QTransform hTransform( H.at(0,0), H.at(1,0), H.at(2,0), H.at(0,1), H.at(1,1), H.at(2,1), H.at(0,2), H.at(1,2), H.at(2,2)); int distance = Settings::getGeneral_multiDetectionRadius(); // in pixels if(Settings::getGeneral_multiDetection()) { // Get the outliers and recompute homography with them matchesList.push_back(threads[j]->getOutliers()); matchesId.push_back(id); // compute distance from previous added same objects... QMultiMap >::iterator objIter = objectsDetected.find(id); for(;objIter!=objectsDetected.end() && objIter.key() == id; ++objIter) { qreal dx = objIter.value().second.m31() - hTransform.m31(); qreal dy = objIter.value().second.m32() - hTransform.m32(); int d = (int)sqrt(dx*dx + dy*dy); if(d < distance) { distance = d; } } } if(distance >= Settings::getGeneral_multiDetectionRadius()) { QRect rect = objects_.value(id)->rect(); objectsDetected.insert(id, QPair(rect, hTransform)); inliers_.insert(id, threads[j]->getInliers()); outliers_.insert(id, threads[j]->getOutliers()); } else { rejectedInliers_.insert(id, threads[j]->getInliers()); rejectedOutliers_.insert(id, threads[j]->getOutliers()); } } else { rejectedInliers_.insert(id, threads[j]->getInliers()); rejectedOutliers_.insert(id, threads[j]->getOutliers()); } } else { rejectedInliers_.insert(id, threads[j]->getInliers()); rejectedOutliers_.insert(id, threads[j]->getOutliers()); } } } timeStamps_.insert(kTimeHomography, time.restart()); } } else if(!objectsDescriptors_.empty() && sceneKeypoints_.size()) { UWARN("Cannot search, objects must be updated"); } } timeStamps_.insert(kTimeTotal, totalTime.elapsed()); return success; }