diff --git a/README.md b/README.md index 9c7eeb3..d769740 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Vapoursynth Connected Components Label Filtering from OpenCV. Using OpenCV's [`connectedComponentsWithStats`](https://docs.opencv.org/5.x/d3/dc0/group__imgproc__shape.html#ga107a78bf7cd25dec05fb4dfc5c9e765f) to compute the connected components labeled image of a boolean image and also get statistics for each label. Then filter the labeled content based on the statistical results. +Require VS API >= 4.0 + ## Usage Exclude the labels **above** `ccl_thr` ``` @@ -14,6 +16,10 @@ Exclude the labels **under** `ccl_thr` ``` core.cv_ccl.ExcludeCCLUnder(clip mask, int cc_thr[, int connectivity, int ccl_type, int cc_stat_type]) ``` +Read all the stats from `cv::connectedComponentsWithStats` to FrameProps +``` +core.cv_ccl.GetCCLStats(clip mask[, int connectivity, int ccl_type]) +``` For example, if you want to remove all the connected components smaller than 2500 pixels (in area) inside your mask clip, you can simply write: ```python @@ -34,6 +40,24 @@ core.cv_ccl.ExcludeCCLUnder(mask, 2500) +If you want to get all the ccl stats data, using `core.cv_ccl.GetCCLStats`, you will get FrameProps like: + +``` +_CCLStatNumLabels +_CCLStatAreas +_CCLStatLefts +_CCLStatTops +_CCLStatWidths +_CCLStatHeights +_CCLStatCentroids_x +_CCLStatCentroids_y +``` + +which should contains array (except `_CCLStatNumLabels`) if the input clip isn't a blank clip. Note that the first label is always the background label, so you will like to get the frame prop like this +```py +f.props.get('_CCLStatAreas', None)[1:] +``` + ***Parameters:*** - **mask** diff --git a/msvc_project/cv_ccl_filter.vcxproj b/msvc_project/cv_ccl_filter.vcxproj index 8aa3a6c..ebfbab1 100644 --- a/msvc_project/cv_ccl_filter.vcxproj +++ b/msvc_project/cv_ccl_filter.vcxproj @@ -156,6 +156,7 @@ + diff --git a/src/CCLGetStats.cpp b/src/CCLGetStats.cpp new file mode 100644 index 0000000..5acad75 --- /dev/null +++ b/src/CCLGetStats.cpp @@ -0,0 +1,116 @@ +#include "shared.h" + +static void process_ccl_stats(const VSFrame* src, VSFrame* dst, const MaskData* const VS_RESTRICT d, const VSAPI* vsapi) { + const int w = vsapi->getFrameWidth(src, 0); + const int h = vsapi->getFrameHeight(src, 0); + ptrdiff_t stride = vsapi->getStride(src, 0); + const uint8_t* maskp = vsapi->getReadPtr(src, 0); + uint8_t* dstp = vsapi->getWritePtr(dst, 0); + VSMap* props = vsapi->getFramePropertiesRW(dst); + + cv::Mat maskImg(h, w, CV_8UC1, (void*)maskp); + cv::Mat labels, stats, centroids; + + auto num_labels = cv::connectedComponentsWithStats(maskImg, labels, stats, centroids, d->connectivity, CV_16U, d->ccl_type); + + // becareful with the number of labels, if there is only one label, it is the background label + vsapi->mapSetInt(props, "_CCLStatNumLabels", num_labels, maReplace); + + // get the stats for each label, if there is only one label, it is the background label, and its data type will be a int/float. + for (int label = 0; label < num_labels; label++) { + + auto area = stats.at(label, cv::CC_STAT_AREA); + auto left = stats.at(label, cv::CC_STAT_LEFT); + auto top = stats.at(label, cv::CC_STAT_TOP); + auto width = stats.at(label, cv::CC_STAT_WIDTH); + auto height = stats.at(label, cv::CC_STAT_HEIGHT); + // get the centroid at label + auto centroidx = centroids.at(label).x; + auto centroidy = centroids.at(label).y; + + vsapi->mapSetInt(props, "_CCLStatAreas", area, maAppend); + vsapi->mapSetInt(props, "_CCLStatLefts", left, maAppend); + vsapi->mapSetInt(props, "_CCLStatTops", top, maAppend); + vsapi->mapSetInt(props, "_CCLStatWidths", width, maAppend); + vsapi->mapSetInt(props, "_CCLStatHeights", height, maAppend); + vsapi->mapSetFloat(props, "_CCLStatCentroids_x", centroidx, maAppend); + vsapi->mapSetFloat(props, "_CCLStatCentroids_y", centroidy, maAppend); + } + + for (int y = 0; y < h; y++) { + memcpy(dstp, maskImg.ptr(y), w); + dstp += stride; + } +} + +static const VSFrame* VS_CC getCCLStatsGetFrame(int n, int activationReason, void* instanceData, void** frameData, VSFrameContext* frameCtx, VSCore* core, const VSAPI* vsapi) { + auto d{ static_cast(instanceData) }; + + if (activationReason == arInitial) { + vsapi->requestFrameFilter(n, d->node, frameCtx); + } + else if (activationReason == arAllFramesReady) { + const VSFrame* src = vsapi->getFrameFilter(n, d->node, frameCtx); + + const VSVideoFormat* fi = vsapi->getVideoFrameFormat(src); + int height = vsapi->getFrameHeight(src, 0); + int width = vsapi->getFrameWidth(src, 0); + VSFrame* dst = vsapi->newVideoFrame(fi, width, height, src, core); + + if (d->vi->format.colorFamily == cfGray) { + process_ccl_stats(src, dst, d, vsapi); + } + + vsapi->freeFrame(src); + return dst; + } + return nullptr; +} + +static void VS_CC getCCLStatsMapFree(void* instanceData, VSCore* core, const VSAPI* vsapi) { + auto d{ static_cast(instanceData) }; + vsapi->freeNode(d->node); + delete d; +} + +void VS_CC getCCLStatsCreate(const VSMap* in, VSMap* out, void* userData, VSCore* core, const VSAPI* vsapi) { + auto d{ std::make_unique() }; + int err{ 0 }; + + d->node = vsapi->mapGetNode(in, "mask", 0, nullptr); + d->vi = vsapi->getVideoInfo(d->node); + + auto ccl_type = vsapi->mapGetIntSaturated(in, "ccl_type", 0, &err); + if (err) + d->ccl_type = cv::CCL_DEFAULT; + else { + if (ccl_type < -1 || ccl_type > 5) { + vsapi->mapSetError(out, "GetCCLStats: ccl_type must be between -1 and 6."); + vsapi->freeNode(d->node); + return; + } + d->ccl_type = ccl_type; + } + + auto connectivity = vsapi->mapGetIntSaturated(in, "connectivity", 0, &err); + if (err) + d->connectivity = 8; + else { + if (connectivity != 4 && connectivity != 8) { + vsapi->mapSetError(out, "GetCCLStats: connectivity must be 4 or 8."); + vsapi->freeNode(d->node); + return; + } + d->connectivity = connectivity; + } + + if (d->vi->format.bytesPerSample != 1 || (d->vi->format.colorFamily != cfGray)) { + vsapi->mapSetError(out, "GetCCLStats: only Gray8 formats supported."); + vsapi->freeNode(d->node); + return; + } + + VSFilterDependency deps[]{ {d->node, rpGeneral} }; + vsapi->createVideoFilter(out, "GetCCLStats", d->vi, getCCLStatsGetFrame, getCCLStatsMapFree, fmParallel, deps, 1, d.get(), core); + d.release(); +} diff --git a/src/shared.cpp b/src/shared.cpp index f906e85..87b957d 100644 --- a/src/shared.cpp +++ b/src/shared.cpp @@ -4,4 +4,5 @@ VS_EXTERNAL_API(void) VapourSynthPluginInit2(VSPlugin* plugin, const VSPLUGINAPI vspapi->configPlugin("com.dtlnor.cv_ccl", "cv_ccl", "Mask connected components label filtering", VS_MAKE_VERSION(1, 0), VAPOURSYNTH_API_VERSION, 0, plugin); vspapi->registerFunction("ExcludeCCLAbove", "mask:vnode;cc_thr:int;connectivity:int:opt;ccl_type:int:opt;cc_stat_type:int:opt;", "clip:vnode;", excludeCCLAboveCreate, nullptr, plugin); vspapi->registerFunction("ExcludeCCLUnder", "mask:vnode;cc_thr:int;connectivity:int:opt;ccl_type:int:opt;cc_stat_type:int:opt;", "clip:vnode;", excludeCCLUnderCreate, nullptr, plugin); + vspapi->registerFunction("GetCCLStats", "mask:vnode;connectivity:int:opt;ccl_type:int:opt;", "clip:vnode;", getCCLStatsCreate, nullptr, plugin); } diff --git a/src/shared.h b/src/shared.h index 36f2c39..29cde1b 100644 --- a/src/shared.h +++ b/src/shared.h @@ -16,4 +16,5 @@ struct MaskData final { }; extern void VS_CC excludeCCLAboveCreate(const VSMap* in, VSMap* out, void* userData, VSCore* core, const VSAPI* vsapi); -extern void VS_CC excludeCCLUnderCreate(const VSMap* in, VSMap* out, void* userData, VSCore* core, const VSAPI* vsapi); \ No newline at end of file +extern void VS_CC excludeCCLUnderCreate(const VSMap* in, VSMap* out, void* userData, VSCore* core, const VSAPI* vsapi); +extern void VS_CC getCCLStatsCreate(const VSMap* in, VSMap* out, void* userData, VSCore* core, const VSAPI* vsapi);