/*! * \file BlobDetector.h * \date 2019/01/31 * * \author Lin, Chi * Contact: lin.chi@hzleaper.com * * * \note */ #ifndef __BlobDetector_h_ #define __BlobDetector_h_ #include "CVUtils.h" #include "StdUtils.h" #include "CyclopsEnums.h" #include "CyclopsModules.h" #include "DetectRoi.h" #include "BlobInstance.h" #include "BlobFilter.h" typedef std::pair BlobColorRange; static inline void operator >> (const FileNode &node, BlobColorRange& cr) { node["first"] >> cr.first; node["second"] >> cr.second; } static inline FileStorage& operator << (FileStorage &fs, BlobColorRange& cr) { fs << "{:" << "first" << cr.first << "second" << cr.second << "}"; return fs; } static inline void operator >> (const FileNode &node, vector& crs) { for (FileNodeIterator it = node.begin(); it != node.end(); ++it) { BlobColorRange cr; *it >> cr; crs.push_back(cr); } } static inline FileStorage& operator << (FileStorage &fs, vector& crs) { fs << "["; for (auto it = crs.begin(); it != crs.end(); ++it) { fs << *it; } fs << "]"; return fs; } /*! \brief Locate single or multiple blobs in the given image and ROI. * * Note: Both greyscale and color source is supported. * You may want to specify the blob color range use addBlobColor(), * or let the algorithm to pick color range automatically using selectBlobColor(). * You can also exclude some background pixels from blob extraction by specifying the background color range. * * 1) If you are using Cyclops as static library, * 1.1) and you want global factory to manage the detector for you, initialize the detector * via BlobDetector::getInstance(). * Example: * \code{.cpp} * BlobDetector::Ptr bdPtr = BlobDetector::getInstance("detect sth"); * bdPtr->reset(3); // on a color image * bdPtr->addBlobColor(Scalar(40, 200, 210), Scalar(50, 255, 255)); // define blob color range * // define two filters * BlobFilter& bf = bdPtr->addFilter(); * bf.setPerimeter(20, 20000); * bf.setMode(BlobFilterMode::Perimeter); * bf = bdPtr->addFilter(); * bf.setWidth(30, 600); * bf.setMode(BlobFilterMode::Width); * BlobInstance bestBlob; // result blob * bool ret = bdPtr->detectBest(gIBImg, bestBlob); * * // delete it later * BlobDetector::deleteInstance("detect sth"); * \endcode * * 1.2) or, if you wish to manage the detector yourself: * \code{.cpp} * BlobDetector::Ptr bdPtr = std::make_shared(); // remember to hold the smart pointer * // balabala, same as above * // ... * \endcode * * 2) If you are using Cyclops as dynamic library, * initialize and manipulate the detector via CyclopsModules APIs. * Example: * \code{.cpp} * // get a long-term detector, and give it an unique name * BlobDetector::Ptr bdPtr = GetModuleInstance("detect sth"); * // get for temporary usage * BlobDetector::Ptr localBdPtr = GetModuleInstance(); * * // delete it later * DeleteModuleInstance("detect sth"); * \endcode * * see BlobTest for unit test */ class BlobDetector : public ICyclopsModuleInstance { public: /*! \fn getChannel * Get number of channel, see also setChannel() */ DECLARE_PARAMETER_GET(int, Channel) /*! \fn setExcludeBoundary * Define whether to exclude blobs that intersect the ROI boundary, default to false, see also getExcludeBoundary() * \fn getExcludeBoundary * Get value of whether to exclude blobs that intersect the ROI boundary, see also setExcludeBoundary() */ DECLARE_PARAMETER(bool, ExcludeBoundary) /*! \fn setFillHole * Define to enable the contribution of holesin the overall blob statistics, default to false, see also getFillHole() * For example, filling holes inside a blob would increase its area. * \fn getFillHole * Get value of to enable the contribution of holesin the overall blob statistics, see also setFillHole() */ DECLARE_PARAMETER(bool, FillHole) /*! \fn setThresholdType * Define threshold method used to separate blob and background, default to ThresholdType::Custom, see also getThresholdType() * \fn getThresholdType * Get value of long_name, see also setThresholdType() */ DECLARE_PARAMETER2(ThresholdType, ThresholdType, ThresholdType::Custom, ThresholdType::LocalAdaptive) /*! \fn setPolarity * Define polarity of blob when global/local auto-thresholding is used, * either black blob on white background(Polarity::BlackOnWhite, default), * or white blob on black background(Polarity::WhiteOnBlack), see also getPolarity() * \fn getPolarity * Get value of polarity of circle edge, see also setPolarity() */ DECLARE_PARAMETER2(Polarity, Polarity, Polarity::BlackOnWhite, Polarity::WhiteOnBlack) /*! \fn setSortBy * Define which property used for sorting the blobs, default to SortBy::Area, see also getSortBy() * \fn getSortBy * Get value of which property used for sorting the blobs, see also setSortBy() */ DECLARE_PARAMETER(SortBy, SortBy) /*! \fn setLocalThresholdBias * Define a constant value added to local theshold, default to 20, see also getLocalThresholdBias() * \fn getLocalThresholdBias * Get value of a constant value added to local theshold, see also setLocalThresholdBias() */ DECLARE_PARAMETER(int, LocalThresholdBias) /*! \fn setSoftThreshold * Define whether to use soft thresholding, default to false, see also getSoftThreshold() * \fn getSoftThreshold * Get value of whether to use soft thresholding, see also setSoftThreshold() */ DECLARE_PARAMETER_GET(bool, UseSoftThreshold) /*! \fn setSoftThresholdRange * Define the weighted range of soft thresholding. For example, if we define blob color range to be [100, 200], * and soft thresholding range to be 5, then, [0, 95] and [205, 255] are background, [105, 195] are foreground, * 96 ~ 104 are weighted as 0.1 ~ 0.9, 196 ~ 204 are weighted as 0.1 ~ 0.9. * Default to 5, see also getSoftThresholdRange() * \fn getSoftThresholdRange * Get value of the weighted range of soft thresholding, see also setSoftThresholdRange() */ DECLARE_PARAMETER_GET(int, SoftThresholdRange) /*! \fn setBlobBounderSize * Define image bounder size of each blob instance, default to 1, see also getBlobBounderSize() * \fn getBlobBounderSize * Get image bounder size of each blob instance, see also setBlobBounderSize() */ DECLARE_PARAMETER2(int, BlobBounderSize, 1, 10) /*! \fn setContourType * Define which type of contour we are going to find, either external or hole contour, default to ContourType::External, * see also getContourType() * \fn getContourType * Get value of which type of contour we are going to find, see also setContourType() */ DECLARE_PARAMETER(ContourType, ContourType) /*! \fn setHierarchy * Define which hierachy we are going to find(0-based), default to -1 means all, see also getHierarchy() * \fn getHierarchy * Get value of which hierachy we are going to find, see also setHierarchy() */ DECLARE_PARAMETER(int, Hierarchy) public: BlobDetector() : mExcludeBoundary(false), mFillHole(false), mSortBy(SortBy::Area), mThresholdType(ThresholdType::Custom), mPolarity(Polarity::BlackOnWhite), mLocalThresholdBias(20), mChannel(1), mUseSoftThreshold(false), mSoftThresholdRange(5), mBlobBounderSize(1), mContourType(ContourType::External), mHierarchy(-1) {} virtual ~BlobDetector () {} /*! \fn serializeToMemory * Serialize the blob detector into a in-memory string, see also deserializeFromMemory() * @param str used to take the output serialization result * @return true for succeed, false for fail * \fn serializeToFile * Serialize the blob detector into a text file, see also deserializeFromFile() * @param filename file name (full path) where we will write the data * @return true for succeed, false for fail * \fn deserializeFromMemory * Deserialize the blob detector from in-memory string, see also serializeToMemory() * @param str in-memory string * @return true for succeed, false for fail * \fn deserializeFromFile * Deserialize the blob detector from a text file, see also serializeToFile() * @param filename file name (full path) where we will read the data * @return true for succeed, false for fail */ DECL_SERIALIZE_FUNCS //! Smart pointer to hold an instance of BlobDetector typedef std::shared_ptr Ptr; DECL_GET_INSTANCE(BlobDetector::Ptr) /*! return whether this is a well initialized */ virtual bool hasInit() const; /*! Reset detector: * 1. Define number of channel, only 1(gray) or 3(color) channel is acceptable * 2. Clear blob and background color setting, and blob filters */ virtual void reset(int cn); /*! Add new color range [c1, c2] for blob. Pixels in this range will be treated as blob, return the index */ virtual int addBlobColor(const Scalar& c1, const Scalar& c2); /*! Add new color range [c1, c2] for background. Pixels in this range will be treated as background, return the index */ virtual int addBackgroundColor(const Scalar& c1, const Scalar& c2); /*! Add a new blob filter */ virtual BlobFilter& addFilter(); /*! Select a color range for blob or background on a image. You should then explicitly add the color range as blob or background color * @param img input image for color selection * @param seedPoint the initial point for flood fill * @param tol maximum relative difference of neighbor pixels that build the selection * @param c1 lower bound of the new added color range * @param c2 upper bound of the new added color range * @param rngMask pass out the selected pixels as a binary mask, null if you don't want it */ virtual void selectColor(const Mat& img, const Point& seedPoint, Scalar& c1, Scalar& c2, Mat* rngMask = nullptr, int tol = 10); vector& getBlobColors() { return mBlobColors; } vector& getBackgroundColors() { return mBackgroundColors; } vector& getFilters() { return mFilters; } BlobColorRange& getBlobColor(int idx) { _ASSERTE(mBlobColors.size() > idx); return mBlobColors[idx]; } BlobColorRange& getBackgroundColor(int idx) { _ASSERTE(mBackgroundColors.size() > idx); return mBackgroundColors[idx]; } BlobFilter& getFilter(int idx) { _ASSERTE(mFilters.size() > idx); return mFilters[idx]; } void deleteBlobColor(int idx) { if (mBlobColors.size() > idx && idx >= 0) mBlobColors.erase(mBlobColors.begin() + idx); } void deleteBackgroundColor(int idx) { if (mBackgroundColors.size() > idx && idx >= 0) mBackgroundColors.erase(mBackgroundColors.begin() + idx); } void deleteFilter(int idx) { if (mFilters.size() > idx && idx >= 0) mFilters.erase(mFilters.begin() + idx); } void clearBlobColors() { mBlobColors.clear(); } void clearBackgroundColors() { mBackgroundColors.clear(); } void clearFilters() { mFilters.clear(); } virtual Mat genColorMask(const Mat& img, const BlobColorRange& cr); virtual void refreshBlobColor(); virtual void refreshBackgroundColor(); virtual void setUseSoftThreshold(bool val); virtual void setSoftThresholdRange(int val); /*! \deprecated Define whether to use auto-thresholding for binarize image: * 0 for not use, -1 for use below auto-value as blob, 1 for above, see also getUseAutoThreshold() * If auto-thresholding is enabled, the customized blob and background colors will be ignored. */ DEPRECATED void setUseAutoThreshold(int val) { if (val == 0) { mThresholdType = ThresholdType::Custom; } else if (val < 0) { mThresholdType = ThresholdType::GlobalAdaptive; mPolarity = Polarity::BlackOnWhite; } else if (val > 0) { mThresholdType = ThresholdType::GlobalAdaptive; mPolarity = Polarity::WhiteOnBlack; } } /*! \deprecated Get value of whether to user auto-thresholding for binarize image, see also setUseAutoThreshold() */ DEPRECATED int getUseAutoThreshold() const { if (mThresholdType == ThresholdType::Custom) return 0; else if (mThresholdType == ThresholdType::GlobalAdaptive && mPolarity == Polarity::BlackOnWhite) return -1; else return 1; } /*! Detect the best blob using the detector. By best, we mean the first ranking blob after applying sort strategy * @param img input image for detection * @param bestBlob output the best blob, return at most 100 results * @param allScores output all blobs on the image as a score map, no matter if it's filtered. Pass null if you don't want it * @param mask input mask for exclude some pixel from detection * @return true for found, false otherwise */ virtual bool detectBest(const Mat& img, BlobInstance& bestBlob, Mat* allScores = nullptr, Mat* mask = nullptr); /** @overload */ virtual bool detectBest(const Mat& img, const vector& roi, BlobInstance& bestBlob, Mat* allScores = nullptr); /** @overload */ virtual bool detectBest(const Mat& img, DetectRoi& droi, BlobInstance& bestBlob, Mat* allScores = nullptr); /*! Detect multiple best blobs using the detector. By best, we mean the front ranking blobs after applying sort strategy * @param img input image for detection * @param bestBlobs output the best blobs, return at most 100 or 2 * maxCount results * @param allScores output all blobs on the image as a score map, no matter if it's filtered. Pass null if you don't want it * @param minCount expected minimum count of the found instances * @param maxCount expected maximum count of the found instances * @param mask input mask for exclude some pixel from detection * @return count of the found instances, it may below minCount but wouldn't exceed maxCount. return 0 if we found nothing good. */ virtual int detectMulti(const Mat& img, vector* bestBlobs, Mat* allScores = nullptr, int minCount = 0, int maxCount = 10, Mat* mask = nullptr); /** @overload */ virtual int detectMulti(const Mat& img, const vector& roi, vector* bestBlobs, Mat* allScores = nullptr, int minCount = 0, int maxCount = 10); /** @overload */ virtual int detectMulti(const Mat& img, DetectRoi& droi, vector* bestBlobs, Mat* allScores = nullptr, int minCount = 0, int maxCount = 10); /*! Draw blob on the image * @param img on what we should draw * @param blob the detected blob get from detectBest * @param drawCountour whether we should draw the contour * @param countourColor the color of contour * @param drawCenter whether we should draw the center, as a small rect * @param centerColor the color of center * @return canvas */ virtual Mat drawBlob(const Mat& img, BlobInstance& blob, bool drawContour, Scalar contourColor, bool drawCenter, Scalar centerColor); /** @overload */ virtual Mat drawBlobs(const Mat& img, vector& blobs, bool drawContour, Scalar contourColor, bool drawCenter, Scalar centerColor); static inline float getPrimePropFromSortBy(BlobInstance& bestBlob, SortBy sortBy) { switch (sortBy) { case SortBy::Area: return bestBlob.getArea(); case SortBy::Length: return bestBlob.getPerimeter(); case SortBy::Width: return bestBlob.getWidth(); case SortBy::Height: return bestBlob.getWidth(); case SortBy::LeftToRight: case SortBy::RightToLeft: return bestBlob.getCenter().x; case SortBy::TopDown: case SortBy::DownTop: return bestBlob.getCenter().y; case SortBy::Score: return bestBlob.getConfidence(); case SortBy::Circularity: return bestBlob.getCircularity(); case SortBy::Convexity: return bestBlob.getConvexity(); case SortBy::Inertia: return bestBlob.getInertia(); default: _ASSERTE(false && "we should never get here"); return 0; } } typedef std::function SortByFunc; static inline SortByFunc getSortByFunc(SortBy sortBy) { #define GenSortByFunc(SortByEnum, BlobProp, CompareOP)\ else if (sortBy == SortBy::SortByEnum) {\ return [](BlobInstance& b1, BlobInstance& b2) {\ if (b1.BlobProp == b2.BlobProp) return b1.getArea() > b2.getArea();\ else return b1.BlobProp CompareOP b2.BlobProp;\ };\ } if (sortBy == SortBy::None) { return nullptr; } GenSortByFunc(LeftToRight, getCenter().x, < ) GenSortByFunc(RightToLeft, getCenter().x, > ) GenSortByFunc(TopDown, getCenter().y, < ) GenSortByFunc(DownTop, getCenter().y, > ) GenSortByFunc(Score, getConfidence(), > ) GenSortByFunc(Area, getArea(), > ) GenSortByFunc(Length, getPerimeter(), > ) GenSortByFunc(Width, getWidth(), > ) GenSortByFunc(Height, getHeight(), > ) GenSortByFunc(Circularity, getCircularity(), > ) GenSortByFunc(Convexity, getConvexity(), > ) GenSortByFunc(Inertia, getInertia(), > ) else { _ASSERTE(false && "we should never get here"); return nullptr; } } private: virtual bool serialize(FileStorage& fs); virtual bool deserialize(const FileNode& fs); Mat binarizeAutoGlobal(const Mat& img); Mat binarizeAutoLocal(const Mat& img); Mat binarizeCustomThres(const Mat& img); private: vector mBlobColors; vector mBackgroundColors; Mat mBlobLUT; Mat mBackgroundLUT; vector mFilters; }; #endif // BlobDetector_h_