diff --git a/dollar/Engine.cpp b/dollar/Engine.cpp index 8e5cd48..321896f 100644 --- a/dollar/Engine.cpp +++ b/dollar/Engine.cpp @@ -53,7 +53,8 @@ static float pathDistance(const std::vector& _path1, const std::vector16, "NB element in a path must be > 16 ..."); setRotationInvariance(false); @@ -67,7 +68,7 @@ void dollar::Engine::setNumberPointInGesture(size_t _value) { m_numPointsInGesture = _value; DOLLAR_ASSERT(m_numPointsInGesture>16, "NB element in a path must be > 16 ..."); for (auto &it: m_gestures) { - it.configure(m_numPointsInGesture/RATIO_START_VECTOR, m_numPointsInGesture, m_paramterIgnoreRotation); + it.configure(m_numPointsInGesture/RATIO_START_VECTOR, m_numPointsInGesture, m_paramterIgnoreRotation, m_PPlusDistance); } } @@ -101,6 +102,121 @@ float dollar::Engine::distanceAtBestAngle(const std::vector& _points, cons return std::min(f1, f2); } +static float cloudDistance(const std::vector& _points1, const std::vector& _points2, size_t _start) { + std::vector matched; + matched.resize(_points1.size(), false); + float out = 0; + size_t iii = _start; + do { + float min = MAX_FLOAT; + size_t index = 0; + for (size_t jjj=0; jjj& _points, const std::vector& _reference) { + float out = MAX_FLOAT; + float si = 0.5f; + float step = pow(_points.size(), si-1); + if (step < 1) { + // DOLLAR_ERROR(" step is too small ... " << step); + step = 1.0f; + } + for (size_t iii=0; iii<_points.size(); iii+=int32_t(step)) { + float d1 = cloudDistance(_points, _reference, iii); + float d2 = cloudDistance(_reference, _points, iii); + out = std::min(out, std::min(d1,d2)); + } + return out; // Distance to the nearest point must be < 2.0 (maximum distance visible) +} + + +static float calculatePPlusDistance(const std::vector& _points, const std::vector& _reference, std::vector& _dataDebug) { + std::vector distance; // note: use square distance (faster, we does not use std::sqrt()) + distance.resize(_points.size(), MAX_FLOAT); + // point Id that is link on the reference. + std::vector usedId; + usedId.resize(_reference.size(), -1); + + for (int32_t iii=0; iii<_points.size(); iii++) { + if (distance[iii] < 100.0) { + continue; + } + float bestDistance = MAX_FLOAT; + int32_t kkkBest = -1; + for (int32_t kkk=0; kkk<_reference.size(); ++kkk) { + float dist = (_points[iii]-_reference[kkk]).length2(); + if (usedId[kkk] != -1) { + if (dist < distance[usedId[kkk]]) { + if (dist < bestDistance) { + bestDistance = dist; + kkkBest = kkk; + } + } + } else { + if (dist < bestDistance) { + bestDistance = dist; + kkkBest = kkk; + } + } + } + if (kkkBest != -1) { + int32_t previous = usedId[kkkBest]; + usedId[kkkBest] = iii; + distance[iii] = bestDistance; + if (previous != -1) { + distance[previous] = MAX_FLOAT; + iii = previous-1; + } + } + } + double fullDistance = 0; + int32_t nbTestNotUsed = 0; + int32_t nbReferenceNotUsed = 0; + // now we count the full distance use and the number of local gesture not use + for (auto &it : distance) { + if (it < 100.0) { + fullDistance = it; + } else { + // TODO : Only for debug + nbTestNotUsed++; + } + } + // TODO : Only for debug + // we count the number of point in the gesture reference not used: + for (auto &it : usedId) { + if (it == -1) { + nbReferenceNotUsed++; + } + } + // TODO : Only for debug + for (int32_t kkk=0; kkk<_reference.size(); ++kkk) { + if (it != -1) { + _dataDebug.push_back(std::make_pair(it, kkk)); + } + } + // TODO : Only for debug + DOLLAR_INFO("test distance : " << fullDistance << " nbTestNotUsed=" << nbTestNotUsed << " nbReferenceNotUsed=" << nbReferenceNotUsed); + return fullDistance; +} + + + float dollar::Engine::optimalCosineDistance(const std::vector& _vect1, const std::vector& _vect2) { if (_vect1.size() != _vect2.size()) { DOLLAR_ERROR("Vector have not the same size: " << _vect1.size() << " != " << _vect2.size()); @@ -152,7 +268,7 @@ bool dollar::Engine::loadGesture(const std::string& _filename) { } void dollar::Engine::addGesture(Gesture _gesture) { - _gesture.configure(m_numPointsInGesture/RATIO_START_VECTOR, m_numPointsInGesture, m_paramterIgnoreRotation); + _gesture.configure(m_numPointsInGesture/RATIO_START_VECTOR, m_numPointsInGesture, m_paramterIgnoreRotation, m_PPlusDistance); m_gestures.push_back(std::move(_gesture)); } @@ -164,13 +280,27 @@ dollar::Results dollar::Engine::recognize(const std::vector& _points, cons #define MAX_RESULT_NUMBER (5) dollar::Results dollar::Engine::recognize(const std::vector>& _strokes, const std::string& _method) { - std::vector points = dollar::combineStrokes(_strokes); - // Make sure we have some templates to compare this to - // or else recognition will be impossible + // Check if we have gestures... if (m_gestures.empty()) { DOLLAR_WARNING("No templates loaded so no symbols to match."); return Results(); } + if ( _method == "$N-protractor" + || _method == "$N" + || _method == "$1") { + return recognizeN(_strokes, _method); + } else if (_method == "$P") { + return recognizeP(_strokes); + } else if (_method == "$P+") { + return recognizePPlus(_strokes); + } + DOLLAR_WARNING("Un-recognise methode ... '" << _method << "' supported: [$1,$N,$N-protractor,$P,$P+]" ); + return Results(); +} + + +dollar::Results dollar::Engine::recognizeN(const std::vector>& _strokes, const std::string& _method) { + std::vector points = dollar::combineStrokes(_strokes); points = dollar::normalizePath(points, m_numPointsInGesture, m_paramterIgnoreRotation); vec2 startv = dollar::getStartVector(points, m_numPointsInGesture/RATIO_START_VECTOR); std::vector vector = normalyse(points); @@ -196,11 +326,11 @@ dollar::Results dollar::Engine::recognize(const std::vector>& } float distance = MAX_FLOAT; // for Protractor - if (_method=="protractor") { - distance = Engine::optimalCosineDistance(vector, gesture.getEngineVector(jjj)); + if (_method=="$p-protractor") { + distance = optimalCosineDistance(vector, gesture.getEngineVector(jjj)); } else { // Golden Section Search (original $N) - distance = Engine::distanceAtBestAngle(points, gesture.getEnginePath(jjj)); + distance = distanceAtBestAngle(points, gesture.getEnginePath(jjj)); } for (size_t kkk=0; kkk>& } } } - // Make sure we actually found a good match - // Sometimes we don't, like when the user doesn't draw enough points + // Check if we have match ... if (-1 == indexOfBestMatch[0]) { DOLLAR_WARNING("Couldn't find a good match."); return Results(); } Results res; - // Turn the distance into a percentage by dividing it by half the maximum possible distance (across the diagonal of the square we scaled everything too) - // Distance = hwo different they are subtract that from 1 (100%) to get the similarity - if (_method == "protractor") { + // transform distance in a % range + if (_method == "$p-protractor") { for (size_t iii=0; iii>& return res; } + +dollar::Results dollar::Engine::recognizeP(const std::vector>& _strokes) { + std::vector points = dollar::combineStrokes(_strokes); + points = dollar::normalizePath(points, m_numPointsInGesture, m_paramterIgnoreRotation); + // Keep maximum 5 results ... + float bestDistance[MAX_RESULT_NUMBER]; + int32_t indexOfBestMatch[MAX_RESULT_NUMBER]; + for (size_t iii=0; iiiint32_t(kkk); --rrr) { + bestDistance[rrr] = bestDistance[rrr-1]; + indexOfBestMatch[rrr] = indexOfBestMatch[rrr-1]; + } + indexOfBestMatch[kkk] = iii; + } + bestDistance[kkk] = distance; + break; + } else { + if (kkk == 0) { + DOLLAR_VERBOSE("[" << iii << "," << jjj << "] d=" << distance << " < bd=" << bestDistance << " "); + } + } + } + } + // Check if we have match ... + if (-1 == indexOfBestMatch[0]) { + DOLLAR_WARNING("Couldn't find a good match."); + return Results(); + } + Results res; + for (size_t iii=0; iii>& _strokes) { + std::vector points = dollar::normalizePathToPoints(_strokes, m_PPlusDistance); + // Keep maximum 5 results ... + float bestDistance[MAX_RESULT_NUMBER]; + int32_t indexOfBestMatch[MAX_RESULT_NUMBER]; + for (size_t iii=0; iii dataPair; + distance = calculatePPlusDistance(points, gesture.getEnginePoints(), dataPair); + for (size_t kkk=0; kkkint32_t(kkk); --rrr) { + bestDistance[rrr] = bestDistance[rrr-1]; + indexOfBestMatch[rrr] = indexOfBestMatch[rrr-1]; + } + indexOfBestMatch[kkk] = iii; + } + bestDistance[kkk] = distance; + break; + } else { + if (kkk == 0) { + DOLLAR_VERBOSE("[" << iii << "] d=" << distance << " < bd=" << bestDistance << " "); + } + } + } + } + // Check if we have match ... + if (-1 == indexOfBestMatch[0]) { + DOLLAR_WARNING("Couldn't find a good match."); + return Results(); + } + Results res; + for (size_t iii=0; iii& _points, const std::vector& _reference); - Results recognize(const std::vector>& _paths, const std::string& _method="normal"); - Results recognize(const std::vector& _points, const std::string& _method="normal"); + Results recognize(const std::vector>& _paths, const std::string& _method="$N"); + Results recognize(const std::vector& _points, const std::string& _method="$N"); float optimalCosineDistance(const std::vector& _vect1, const std::vector& _vect2); bool loadPath(const std::string& _path); bool loadGesture(const std::string& _filename); void addGesture(Gesture _gesture); + private: + Results recognizeN(const std::vector>& _paths, const std::string& _method="$N"); + Results recognizeP(const std::vector>& _paths); + Results recognizePPlus(const std::vector>& _paths); }; } diff --git a/dollar/Gesture.cpp b/dollar/Gesture.cpp index 67c61c5..b56c45c 100644 --- a/dollar/Gesture.cpp +++ b/dollar/Gesture.cpp @@ -124,16 +124,12 @@ void dollar::Gesture::storeJSON(const std::string& _fileName) { doc.store(_fileName); } -void dollar::Gesture::storeSVG(const std::string& _fileName) { +void dollar::Gesture::storeSVG(const std::string& _fileName, bool _storeDot) { std::vector> strokes = dollar::scaleToOne(m_path); std::string data("\n"); data += "\n"; for (auto &itLines : strokes) { - data += " \n"; + } + } data += "\n"; etk::FSNodeWriteAllData(_fileName, data); } - -void dollar::Gesture::configure(float _startAngleIndex, size_t _nbSample, bool _ignoreRotation) { +void dollar::Gesture::set(const std::string& _name, uint32_t _subId, std::vector> _path) { + m_name = _name; + m_subId = _subId; + m_path = _path; m_enginePath.clear(); m_engineVector.clear(); m_engineStartV.clear(); + m_enginePoints.clear(); +} + +void dollar::Gesture::configure(float _startAngleIndex, size_t _nbSample, bool _ignoreRotation, float _distance) { + m_enginePath.clear(); + m_engineVector.clear(); + m_engineStartV.clear(); + m_enginePoints.clear(); + // Generates dots: + m_enginePoints = dollar::normalizePathToPoints(m_path, _distance); + DOLLAR_INFO("create " << m_enginePoints.size() << " points"); + // for debug only + storeSVG("out/zzz_" + m_name + "_" + etk::to_string(m_subId) + ".svg", true); // Simplyfy paths std::vector> uniPath = dollar::makeReferenceStrokes(m_path); // normalize paths diff --git a/dollar/Gesture.h b/dollar/Gesture.h index 02f7b21..f6db3ab 100644 --- a/dollar/Gesture.h +++ b/dollar/Gesture.h @@ -18,11 +18,12 @@ namespace dollar { Gesture(); bool load(const std::string& _filename); bool store(const std::string& _filename); + void set(const std::string& _name, uint32_t _subId, std::vector> _path); protected: bool loadJSON(const std::string& _filename); bool loadSVG(const std::string& _filename); void storeJSON(const std::string& _filename); - void storeSVG(const std::string& _filename); + void storeSVG(const std::string& _filename, bool _storeDot=false); public: const std::string& getName() { return m_name; @@ -37,9 +38,10 @@ namespace dollar { std::vector> m_enginePath; // Singulized path with every conbinaison std::vector> m_engineVector; std::vector m_engineStartV; + std::vector m_enginePoints; public: // Configure the reference gesture for recognition... - void configure(float _startAngleIndex, size_t _nbSample, bool _ignoreRotation); + void configure(float _startAngleIndex, size_t _nbSample, bool _ignoreRotation, float _distance); size_t getEngineSize() const { return m_enginePath.size(); } @@ -52,6 +54,9 @@ namespace dollar { const vec2& getEngineStartVector(size_t _id) const { return m_engineStartV[_id]; } + const std::vector& getEnginePoints() const { + return m_enginePoints; + } }; std::vector> loadPoints(const std::string& _fileName); } diff --git a/dollar/tools.cpp b/dollar/tools.cpp index f88fccb..2702ce6 100644 --- a/dollar/tools.cpp +++ b/dollar/tools.cpp @@ -103,21 +103,18 @@ std::vector dollar::translateBariCenterToZero(std::vector _points) { return out; } - -std::vector dollar::resample(std::vector _points, int32_t _nbPoints) { +static std::vector discretize(std::vector _points, float _interval) { std::vector out; - // calculate the interval between every points ... - float interval = pathLength(_points) / (_nbPoints - 1); - float distance = 0.0; // same first point ==> no change out.push_back(_points.front()); + float distance = 0.0f; // For all other point we have to resample elements for (size_t iii=1; iii<_points.size(); ++iii) { vec2 currentPoint = _points[iii]; vec2 previousPoint = _points[iii-1]; float tmpDist = (currentPoint-previousPoint).length(); - if ((distance + tmpDist) >= interval) { - vec2 point = previousPoint + (currentPoint - previousPoint) * ((interval - distance) / tmpDist); + if ((distance + tmpDist) >= _interval) { + vec2 point = previousPoint + (currentPoint - previousPoint) * ((_interval - distance) / tmpDist); out.push_back(point); _points.insert(_points.begin() + iii, point); distance = 0.0; @@ -125,6 +122,14 @@ std::vector dollar::resample(std::vector _points, int32_t _nbPoints) distance += tmpDist; } } + return out; +} + +std::vector dollar::resample(std::vector _points, int32_t _nbPoints) { + std::vector out; + // calculate the interval between every points ... + float interval = pathLength(_points) / (_nbPoints - 1); + out = discretize(_points, interval); // somtimes we fall a rounding-error short of adding the last point, so add it if so if (int64_t(out.size()) == (_nbPoints - 1)) { out.push_back(_points.back()); @@ -205,5 +210,29 @@ std::vector dollar::normalizePath(std::vector _points, size_t _nbSam _points = rotateToZero(_points); } _points = scaleToOne(_points); - return translateBariCenterToZero(_points);; + return translateBariCenterToZero(_points); } + + +std::vector dollar::normalizePathToPoints(std::vector> _points, float _distance) { + // Scale point to (0.0,0.0) position and (1.0,1.0) size + _points = dollar::scaleToOne(_points); + std::vector out; + for (auto &it : _points) { + if (it.size() == 0) { + continue; + } + if (it.size() == 1) { + out.push_back(it[0]); + continue; + } + std::vector tmp = discretize(it, _distance); + for (auto &pointIt : tmp) { + out.push_back(pointIt); + } + if (tmp[tmp.size()-1] != it[it.size()-1]) { + out.push_back(it[it.size()-1]); + } + } + return translateBariCenterToZero(out); +} \ No newline at end of file diff --git a/dollar/tools.h b/dollar/tools.h index 8cc8ccb..2b36b5a 100644 --- a/dollar/tools.h +++ b/dollar/tools.h @@ -101,6 +101,15 @@ namespace dollar { * @return new list of points */ std::vector normalizePath(std::vector _points, size_t _nbSample, bool _ignoreRotation); + + /** + * @brief Transform the path to be comparable, resample the path with a specific number of sample, and limit size at 1.0 square center around 0 + * @note The difference with @ref normalizePath is thet we do not combinethe path together, that permit to not have unneded point between strokes... + * @param[in] _points List of points in the path + * @param[in] _distance Distance between points + * @return new list of points + */ + std::vector normalizePathToPoints(std::vector> _points, float _distance); } diff --git a/test/main.cpp b/test/main.cpp index 19eb54a..dc4dd19 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -20,13 +20,19 @@ int main(int _argc, const char *_argv[]) { return RUN_ALL_TESTS(); } +TEST(TestAll, plop) { + dollar::Gesture gest; + gest.set("test", 55, dollar::loadPoints("DATA:test/P.json")); + gest.configure(0.1, 64, false, 0.1f); +} + /* * single-stroke gesture recognition */ TEST(TestAll, singleStroke_normal) { dollar::Engine reco; reco.loadPath("DATA:figure"); - dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/Arrow.json"), "normal"); + dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/Arrow.json"), "$N"); EXPECT_EQ(res.haveMath(), true); if (res.haveMath() == false) { TEST_INFO(" Recognise noting ..."); @@ -42,7 +48,7 @@ TEST(TestAll, singleStroke_normal) { TEST(TestAll, singleStroke_protractor) { dollar::Engine reco; reco.loadPath("DATA:figure"); - dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/Arrow.json"), "protractor"); + dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/Arrow.json"), "$N-protractor"); EXPECT_EQ(res.haveMath(), true); if (res.haveMath() == false) { TEST_INFO(" Recognise noting ..."); @@ -62,7 +68,7 @@ TEST(TestAll, singleStroke_protractor) { TEST(TestAll, multiStroke_normal) { dollar::Engine reco; reco.loadPath("DATA:text"); - dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/P.json"), "normal"); + dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/P.json"), "$N"); EXPECT_EQ(res.haveMath(), true); if (res.haveMath() == false) { TEST_INFO(" Recognise noting ..."); @@ -78,7 +84,43 @@ TEST(TestAll, multiStroke_normal) { TEST(TestAll, multiStroke_protractor) { dollar::Engine reco; reco.loadPath("DATA:text"); - dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/P.json"), "protractor"); + dollar::Results res = reco.recognize(dollar::loadPoints("DATA:test/P.json"), "$N-protractor"); + EXPECT_EQ(res.haveMath(), true); + if (res.haveMath() == false) { + TEST_INFO(" Recognise noting ..."); + return; + } + EXPECT_EQ(res.getName(), "P"); + TEST_INFO("Results"); + for (size_t iii=0; iii