/* Copyright (c) 2011-2014, Mathieu Labbe - IntRoLab - Universite de Sherbrooke All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Universite de Sherbrooke nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "find_object/FindObject.h" #include "find_object/Settings.h" #include "find_object/utilite/ULogger.h" #include "ObjSignature.h" #include "utilite/UDirectory.h" #include "Vocabulary.h" #include #include #include #include #include #include #include namespace find_object { FindObject::FindObject(bool keepImagesInRAM, QObject * parent) : QObject(parent), vocabulary_(new Vocabulary()), detector_(Settings::createKeypointDetector()), extractor_(Settings::createDescriptorExtractor()), sessionModified_(false), keepImagesInRAM_(keepImagesInRAM) { qRegisterMetaType("find_object::DetectionInfo"); UASSERT(detector_ != 0 && extractor_ != 0); } FindObject::~FindObject() { delete detector_; delete extractor_; delete vocabulary_; objectsDescriptors_.clear(); } bool FindObject::loadSession(const QString & path) { if(QFile::exists(path) && !path.isEmpty() && QFileInfo(path).suffix().compare("bin") == 0) { QFile file(path); file.open(QIODevice::ReadOnly); QDataStream in(&file); ParametersMap parameters; // load parameters in >> parameters; for(QMap::iterator iter=parameters.begin(); iter!=parameters.end(); ++iter) { Settings::setParameter(iter.key(), iter.value()); } // save vocabulary vocabulary_->load(in); // load objects while(!in.atEnd()) { ObjSignature * obj = new ObjSignature(); obj->load(in, !keepImagesInRAM_); if(obj->id() >= 0) { objects_.insert(obj->id(), obj); } else { UERROR("Failed to load and object!"); delete obj; } } file.close(); if(!Settings::getGeneral_invertedSearch()) { // this will fill objectsDescriptors_ matrix updateVocabulary(); } sessionModified_ = false; return true; } else { UERROR("Invalid session file (should be *.bin): \"%s\"", path.toStdString().c_str()); } return false; } bool FindObject::saveSession(const QString & path) { if(!path.isEmpty() && QFileInfo(path).suffix().compare("bin") == 0) { QFile file(path); file.open(QIODevice::WriteOnly); QDataStream out(&file); // save parameters out << Settings::getParameters(); // save vocabulary vocabulary_->save(out); // save objects for(QMultiMap::const_iterator iter=objects_.constBegin(); iter!=objects_.constEnd(); ++iter) { iter.value()->save(out); } file.close(); sessionModified_ = false; return true; } UERROR("Path \"%s\" not valid (should be *.bin)", path.toStdString().c_str()); return false; } int FindObject::loadObjects(const QString & dirPath, bool recursive) { int loadedObjects = 0; QString formats = Settings::getGeneral_imageFormats().remove('*').remove('.'); QStringList paths; paths.append(dirPath); while(paths.size()) { QString currentDir = paths.front(); UDirectory dir(currentDir.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) { if(this->addObject((currentDir.toStdString()+dir.separator()+*iter).c_str())) { ++loadedObjects; } } } paths.pop_front(); if(recursive) { QDir d(currentDir); QStringList subDirs = d.entryList(QDir::AllDirs|QDir::NoDotAndDotDot, QDir::Name); for(int i=subDirs.size()-1; i>=0; --i) { paths.prepend(currentDir+QDir::separator()+subDirs[i]); } } } if(loadedObjects) { this->updateObjects(); this->updateVocabulary(); } 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, filePath); } } return 0; } const ObjSignature * FindObject::addObject(const cv::Mat & image, int id, const QString & filePath) { UASSERT(id >= 0); ObjSignature * s = new ObjSignature(id, image, filePath); if(!this->addObject(s)) { delete s; return 0; } return s; } bool FindObject::addObject(ObjSignature * obj) { UASSERT(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); 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::addObjectAndUpdate(const cv::Mat & image, int id, const QString & filePath) { const ObjSignature * s = this->addObject(image, id, filePath); if(s) { QList ids; ids.push_back(s->id()); updateObjects(ids); updateVocabulary(); } } void FindObject::removeObjectAndUpdate(int id) { if(objects_.contains(id)) { delete objects_.value(id); objects_.remove(id); } updateVocabulary(); } void FindObject::updateDetectorExtractor() { delete detector_; delete extractor_; detector_ = Settings::createKeypointDetector(); extractor_ = Settings::createDescriptorExtractor(); UASSERT(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; } // taken from ASIFT example https://github.com/Itseez/opencv/blob/master/samples/python2/asift.py // affine - is an affine transform matrix from skew_img to img void FindObject::affineSkew( float tilt, float phi, const cv::Mat & image, cv::Mat & skewImage, cv::Mat & skewMask, cv::Mat & Ai) { float h = image.rows; float w = image.cols; cv::Mat A = cv::Mat::zeros(2,3,CV_32FC1); A.at(0,0) = A.at(1,1) = 1; skewMask = cv::Mat::ones(h, w, CV_8U) * 255; if(phi != 0.0) { phi = phi*CV_PI/180.0f; // deg2rad float s = std::sin(phi); float c = std::cos(phi); cv::Mat A22 = (cv::Mat_(2, 2) << c, -s, s, c); cv::Mat cornersIn = (cv::Mat_(4, 2) << 0,0, w,0, w,h, 0,h); cv::Mat cornersOut = cornersIn * A22.t(); cv::Rect rect = cv::boundingRect(cornersOut.reshape(2,4)); A = (cv::Mat_(2, 3) << c, -s, -rect.x, s, c, -rect.y); cv::warpAffine(image, skewImage, A, cv::Size(rect.width, rect.height), cv::INTER_LINEAR, cv::BORDER_REPLICATE); } else { skewImage = image; } if(tilt != 1.0) { float s = 0.8*std::sqrt(tilt*tilt-1); cv::Mat out, out2; cv::GaussianBlur(skewImage, out, cv::Size(0, 0), s, 0.01); cv::resize(out, out2, cv::Size(0, 0), 1.0/tilt, 1.0, cv::INTER_NEAREST); skewImage = out2; A.row(0) /= tilt; } if(phi != 0.0 || tilt != 1.0) { cv::Mat mask = skewMask; cv::warpAffine(mask, skewMask, A, skewImage.size(), cv::INTER_NEAREST); } cv::invertAffineTransform(A, Ai); } class AffineExtractionThread : public QThread { public: AffineExtractionThread( KeypointDetector * detector, DescriptorExtractor * extractor, const cv::Mat & image, float tilt, float phi) : detector_(detector), extractor_(extractor), image_(image), tilt_(tilt), phi_(phi), timeSkewAffine_(0), timeDetection_(0), timeExtraction_(0), timeSubPix_(0) { UASSERT(detector && extractor); } const cv::Mat & image() const {return image_;} const std::vector & keypoints() const {return keypoints_;} const cv::Mat & descriptors() const {return descriptors_;} int timeSkewAffine() const {return timeSkewAffine_;} int timeDetection() const {return timeDetection_;} int timeExtraction() const {return timeExtraction_;} int timeSubPix() const {return timeSubPix_;} protected: virtual void run() { QTime timeStep; timeStep.start(); cv::Mat skewImage, skewMask, Ai; FindObject::affineSkew(tilt_, phi_, image_, skewImage, skewMask, Ai); timeSkewAffine_=timeStep.restart(); //Detect features detector_->detect(skewImage, keypoints_, skewMask); if(keypoints_.size()) { int maxFeatures = Settings::getFeature2D_3MaxFeatures(); if(maxFeatures > 0 && (int)keypoints_.size() > maxFeatures) { keypoints_ = limitKeypoints(keypoints_, maxFeatures); } timeDetection_=timeStep.restart(); //Extract descriptors extractor_->compute(skewImage, keypoints_, descriptors_); timeExtraction_=timeStep.restart(); // Transform points to original image coordinates for(unsigned int i=0; i(3, 1) << keypoints_[i].pt.x, keypoints_[i].pt.y, 1); cv::Mat pa = Ai * p; keypoints_[i].pt.x = pa.at(0,0); keypoints_[i].pt.y = pa.at(1,0); } if(keypoints_.size() && Settings::getFeature2D_6SubPix()) { // Sub pixel should be done after descriptors extraction std::vector corners; cv::KeyPoint::convert(keypoints_, corners); cv::cornerSubPix(image_, corners, cv::Size(Settings::getFeature2D_7SubPixWinSize(), Settings::getFeature2D_7SubPixWinSize()), cv::Size(-1,-1), cv::TermCriteria( CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, Settings::getFeature2D_8SubPixIterations(), Settings::getFeature2D_9SubPixEps() )); UASSERT(corners.size() == keypoints_.size()); for(unsigned int i=0; i keypoints_; cv::Mat descriptors_; int timeSkewAffine_; int timeDetection_; int timeExtraction_; int timeSubPix_; }; class ExtractFeaturesThread : public QThread { public: ExtractFeaturesThread( KeypointDetector * detector, DescriptorExtractor * extractor, int objectId, const cv::Mat & image) : detector_(detector), extractor_(extractor), objectId_(objectId), image_(image), timeSkewAffine_(0), timeDetection_(0), timeExtraction_(0), timeSubPix_(0) { UASSERT(detector && extractor); } 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_;} int timeSkewAffine() const {return timeSkewAffine_;} int timeDetection() const {return timeDetection_;} int timeExtraction() const {return timeExtraction_;} int timeSubPix() const {return timeSubPix_;} protected: virtual void run() { QTime time; time.start(); UINFO("Extracting descriptors from object %d...", objectId_); QTime timeStep; timeStep.start(); if(!Settings::getFeature2D_4Affine()) { keypoints_.clear(); descriptors_ = cv::Mat(); detector_->detect(image_, keypoints_); if(keypoints_.size()) { int maxFeatures = Settings::getFeature2D_3MaxFeatures(); if(maxFeatures > 0 && (int)keypoints_.size() > maxFeatures) { int previousCount = (int)keypoints_.size(); keypoints_ = limitKeypoints(keypoints_, maxFeatures); UDEBUG("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); } timeDetection_+=timeStep.restart(); try { extractor_->compute(image_, keypoints_, descriptors_); } catch(cv::Exception & e) { UERROR("Descriptor exception: %s. Maybe some keypoints are invalid " "for the selected descriptor extractor.", e.what()); descriptors_ = cv::Mat(); keypoints_.clear(); } timeExtraction_+=timeStep.restart(); if((int)keypoints_.size() != descriptors_.rows) { UERROR("obj=%d kpt=%d != descriptors=%d", objectId_, (int)keypoints_.size(), descriptors_.rows); } else if(Settings::getFeature2D_6SubPix()) { // Sub pixel should be done after descriptors extraction std::vector corners; cv::KeyPoint::convert(keypoints_, corners); cv::cornerSubPix(image_, corners, cv::Size(Settings::getFeature2D_7SubPixWinSize(), Settings::getFeature2D_7SubPixWinSize()), cv::Size(-1,-1), cv::TermCriteria( CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, Settings::getFeature2D_8SubPixIterations(), Settings::getFeature2D_9SubPixEps() )); UASSERT(corners.size() == keypoints_.size()); for(unsigned int i=0; i tilts; std::vector phis; tilts.push_back(1.0f); phis.push_back(0.0f); int nTilt = Settings::getFeature2D_5AffineCount(); for(int t=1; t threads; for(unsigned int k=i; kstart(); } for(int k=0; kwait(); keypoints_.insert(keypoints_.end(), threads[k]->keypoints().begin(), threads[k]->keypoints().end()); descriptors_.push_back(threads[k]->descriptors()); timeSkewAffine_ += threads[k]->timeSkewAffine(); timeDetection_ += threads[k]->timeDetection(); timeExtraction_ += threads[k]->timeExtraction(); timeSubPix_ += threads[k]->timeSubPix(); } } } UINFO("%d descriptors extracted from object %d (in %d ms)", descriptors_.rows, objectId_, time.elapsed()); } private: KeypointDetector * detector_; DescriptorExtractor * extractor_; int objectId_; cv::Mat image_; std::vector keypoints_; cv::Mat descriptors_; int timeSkewAffine_; int timeDetection_; int timeExtraction_; int timeSubPix_; }; void FindObject::updateObjects(const QList & ids) { QList objectsList; if(ids.size()) { 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()); if(!keepImagesInRAM_) { objects_.value(id)->removeImage(); } } } UINFO("Features extraction from %d objects... done! (%d ms)", objectsList.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()) { sessionModified_ = true; 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) { UASSERT(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, const cv::Mat & imageA, // image only required if opticalFlow is on const cv::Mat & imageB) : // image only required if opticalFlow is on matches_(matches), objectId_(objectId), kptsA_(kptsA), kptsB_(kptsB), imageA_(imageA), imageB_(imageB), code_(DetectionInfo::kRejectedUndef) { UASSERT(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_;} DetectionInfo::RejectedCode rejectedCode() const {return code_;} 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()) { if(Settings::getHomography_opticalFlow()) { UASSERT(!imageA_.empty() && !imageB_.empty()); cv::Mat imageA = imageA_; cv::Mat imageB = imageB_; if(imageA_.cols < imageB_.cols && imageA_.rows < imageB_.rows) { // padding, optical flow wants images of the same size imageA = cv::Mat::zeros(imageB_.size(), imageA_.type()); imageA_.copyTo(imageA(cv::Rect(0,0,imageA_.cols, imageA_.rows))); } if(imageA.size() == imageB.size()) { //refine matches std::vector status; std::vector err; cv::calcOpticalFlowPyrLK( imageA, imageB_, mpts_1, mpts_2, status, err, cv::Size(Settings::getHomography_opticalFlowWinSize(), Settings::getHomography_opticalFlowWinSize()), Settings::getHomography_opticalFlowMaxLevel(), cv::TermCriteria(cv::TermCriteria::COUNT+cv::TermCriteria::EPS, Settings::getHomography_opticalFlowIterations(), Settings::getHomography_opticalFlowEps()), cv::OPTFLOW_LK_GET_MIN_EIGENVALS | cv::OPTFLOW_USE_INITIAL_FLOW, 1e-4); } else { UERROR("Object's image should be less/equal size of the scene image to use Optical Flow."); } } 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_; cv::Mat imageA_; cv::Mat imageB_; DetectionInfo::RejectedCode code_; std::vector indexesA_; std::vector indexesB_; std::vector outlierMask_; QMultiMap inliers_; QMultiMap outliers_; cv::Mat h_; }; void FindObject::detect(const cv::Mat & image) { QTime time; time.start(); DetectionInfo info; this->detect(image, info); if(info.objDetected_.size() > 0 || Settings::getGeneral_sendNoObjDetectedEvents()) { Q_EMIT objectsFound(info); } if(info.objDetected_.size() > 1) { UINFO("(%s) %d objects detected! (%d ms)", QTime::currentTime().toString("HH:mm:ss.zzz").toStdString().c_str(), (int)info.objDetected_.size(), time.elapsed()); } else if(info.objDetected_.size() == 1) { UINFO("(%s) Object %d detected! (%d ms)", QTime::currentTime().toString("HH:mm:ss.zzz").toStdString().c_str(), (int)info.objDetected_.begin().key(), time.elapsed()); } else if(Settings::getGeneral_sendNoObjDetectedEvents()) { UINFO("(%s) No objects detected. (%d ms)", QTime::currentTime().toString("HH:mm:ss.zzz").toStdString().c_str(), time.elapsed()); } } bool FindObject::detect(const cv::Mat & image, find_object::DetectionInfo & info) { QTime totalTime; totalTime.start(); // reset statistics info = DetectionInfo(); 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; } // DETECT FEATURES AND EXTRACT DESCRIPTORS UDEBUG("DETECT FEATURES AND EXTRACT DESCRIPTORS FROM THE SCENE"); ExtractFeaturesThread extractThread(detector_, extractor_, -1, grayscaleImg); extractThread.start(); extractThread.wait(); info.sceneKeypoints_ = extractThread.keypoints(); info.sceneDescriptors_ = extractThread.descriptors(); info.timeStamps_.insert(DetectionInfo::kTimeKeypointDetection, extractThread.timeDetection()); info.timeStamps_.insert(DetectionInfo::kTimeDescriptorExtraction, extractThread.timeExtraction()); info.timeStamps_.insert(DetectionInfo::kTimeSubPixelRefining, extractThread.timeSubPix()); info.timeStamps_.insert(DetectionInfo::kTimeSkewAffine, extractThread.timeSkewAffine()); bool consistentNNData = (vocabulary_->size()!=0 && vocabulary_->wordToObjects().begin().value()!=-1 && Settings::getGeneral_invertedSearch()) || ((vocabulary_->size()==0 || vocabulary_->wordToObjects().begin().value()==-1) && !Settings::getGeneral_invertedSearch()); bool descriptorsValid = !Settings::getGeneral_invertedSearch() && !objectsDescriptors_.empty() && objectsDescriptors_.begin().value().cols == info.sceneDescriptors_.cols && objectsDescriptors_.begin().value().type() == info.sceneDescriptors_.type(); bool vocabularyValid = Settings::getGeneral_invertedSearch() && vocabulary_->size() && !vocabulary_->indexedDescriptors().empty() && vocabulary_->indexedDescriptors().cols == info.sceneDescriptors_.cols && vocabulary_->indexedDescriptors().type() == info.sceneDescriptors_.type(); // COMPARE UDEBUG("COMPARE"); if((descriptorsValid || vocabularyValid) && info.sceneKeypoints_.size() && consistentNNData) { success = true; QTime time; time.start(); QMultiMap words; if(!Settings::getGeneral_invertedSearch()) { vocabulary_->clear(); // CREATE INDEX for the scene UDEBUG("CREATE INDEX FOR THE SCENE"); words = vocabulary_->addWords(info.sceneDescriptors_, -1, Settings::getGeneral_vocabularyIncremental()); if(!Settings::getGeneral_vocabularyIncremental()) { vocabulary_->update(); } info.timeStamps_.insert(DetectionInfo::kTimeIndexing, time.restart()); } for(QMap::iterator iter=objects_.begin(); iter!=objects_.end(); ++iter) { info.matches_.insert(iter.key(), QMultiMap()); } if(Settings::getGeneral_invertedSearch() || Settings::getGeneral_threads() == 1) { cv::Mat results; cv::Mat dists; // DO NEAREST NEIGHBOR UDEBUG("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(info.sceneDescriptors_.rows, k, CV_32SC1); // results index dists = cv::Mat(info.sceneDescriptors_.rows, k, CV_32FC1); // Distance results are CV_32FC1 vocabulary_->search(info.sceneDescriptors_, results, dists, k); } // PROCESS RESULTS UDEBUG("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() && dists.at(i,0) >= 0.0f) { matched = true; // no criterion, match to the nearest descriptor } if(info.minMatchedDistance_ == -1 || info.minMatchedDistance_ > dists.at(i,0)) { info.minMatchedDistance_ = dists.at(i,0); } if(info.maxMatchedDistance_ == -1 || info.maxMatchedDistance_ < dists.at(i,0)) { info.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) { info.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) { info.matches_.find(objectId).value().insert(objectDescriptorIndex, words.value(wordId)); } } } } } else { //multi-threaded, match objects to scene UDEBUG("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(); info.matches_[threads[k]->getObjectId()] = threads[k]->getMatches(); if(info.minMatchedDistance_ == -1 || info.minMatchedDistance_ > threads[k]->getMinMatchedDistance()) { info.minMatchedDistance_ = threads[k]->getMinMatchedDistance(); } if(info.maxMatchedDistance_ == -1 || info.maxMatchedDistance_ < threads[k]->getMaxMatchedDistance()) { info.maxMatchedDistance_ = threads[k]->getMaxMatchedDistance(); } delete threads[k]; } } } info.timeStamps_.insert(DetectionInfo::kTimeMatching, time.restart()); // Homographies if(Settings::getHomography_homographyComputed()) { // HOMOGRAPHY UDEBUG("COMPUTE HOMOGRAPHY"); int threadCounts = Settings::getGeneral_threads(); if(threadCounts == 0) { threadCounts = info.matches_.size(); } QList matchesId = info.matches_.keys(); QList > matchesList = info.matches_.values(); for(int i=0; i threads; for(int k=i; kkeypoints(), &info.sceneKeypoints_, objects_.value(objectId)->image(), grayscaleImg)); threads.back()->start(); } for(int j=0; jwait(); int id = threads[j]->getObjectId(); QTransform hTransform; DetectionInfo::RejectedCode code = DetectionInfo::kRejectedUndef; if(threads[j]->getHomography().empty()) { code = threads[j]->rejectedCode(); } if(code == DetectionInfo::kRejectedUndef && threads[j]->getInliers().size() < Settings::getHomography_minimumInliers() ) { code = DetectionInfo::kRejectedLowInliers; } if(code == DetectionInfo::kRejectedUndef) { const cv::Mat & H = threads[j]->getHomography(); hTransform = QTransform( 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)); // is homography valid? // Here we use mapToScene() from QGraphicsItem instead // of QTransform::map() because if the homography is not valid, // huge errors are set by the QGraphicsItem and not by QTransform::map(); QRectF objectRect = objects_.value(id)->rect(); QGraphicsRectItem item(objectRect); item.setTransform(hTransform); QPolygonF rectH = item.mapToScene(item.rect()); // If a point is outside of 2x times the surface of the scene, homography is invalid. for(int p=0; p image.cols*2 && rectH.at(p).x() > objectRect.width()*2) || (rectH.at(p).y() < -image.rows && rectH.at(p).x() < -objectRect.height()) || (rectH.at(p).y() > image.rows*2 && rectH.at(p).x() > objectRect.height()*2)) { code= DetectionInfo::kRejectedNotValid; break; } } // angle if(code == DetectionInfo::kRejectedUndef && Settings::getHomography_minAngle() > 0) { for(int a=0; a 180.0-minAngle) { code = DetectionInfo::kRejectedByAngle; break; } } } // multi detection if(code == DetectionInfo::kRejectedUndef && Settings::getGeneral_multiDetection()) { int distance = Settings::getGeneral_multiDetectionRadius(); // in pixels // 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 = info.objDetected_.find(id); for(;objIter!=info.objDetected_.end() && objIter.key() == id; ++objIter) { qreal dx = objIter.value().m31() - hTransform.m31(); qreal dy = objIter.value().m32() - hTransform.m32(); int d = (int)sqrt(dx*dx + dy*dy); if(d < distance) { distance = d; } } if(distance < Settings::getGeneral_multiDetectionRadius()) { code = DetectionInfo::kRejectedSuperposed; } } // Corners visible if(code == DetectionInfo::kRejectedUndef && Settings::getHomography_allCornersVisible()) { // Now verify if all corners are in the scene QRectF sceneRect(0,0,image.cols, image.rows); for(int p=0; prect().size()); info.objDetectedInliers_.insert(id, threads[j]->getInliers()); info.objDetectedOutliers_.insert(id, threads[j]->getOutliers()); info.objDetectedInliersCount_.insert(id, threads[j]->getInliers().size()); info.objDetectedOutliersCount_.insert(id, threads[j]->getOutliers().size()); info.objDetectedFilePaths_.insert(id, objects_.value(id)->filePath()); } else { //Rejected! info.rejectedInliers_.insert(id, threads[j]->getInliers()); info.rejectedOutliers_.insert(id, threads[j]->getOutliers()); info.rejectedCodes_.insert(id, code); } } } info.timeStamps_.insert(DetectionInfo::kTimeHomography, time.restart()); } } else if((descriptorsValid || vocabularyValid) && info.sceneKeypoints_.size()) { UWARN("Cannot search, objects must be updated"); } else if(info.sceneKeypoints_.size() == 0) { // Accept but warn the user UWARN("No features detected in the scene!?!"); success = true; } } info.timeStamps_.insert(DetectionInfo::kTimeTotal, totalTime.elapsed()); return success; } } // namespace find_object