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);