Skip to content

Commit

Permalink
🚀 Improved image processing speed by optimizing the thinning algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
sturkmen72 committed Oct 3, 2024
1 parent 80f1ca2 commit 66cf9b3
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 173 deletions.
61 changes: 18 additions & 43 deletions modules/ximgproc/include/opencv2/ximgproc.hpp
Original file line number Diff line number Diff line change
@@ -1,38 +1,6 @@
/*
* By downloading, copying, installing or using the software you agree to this license.
* If you do not agree to this license, do not download, install,
* copy or use the software.
*
*
* License Agreement
* For Open Source Computer Vision Library
* (3 - clause BSD License)
*
* 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 names of the copyright holders nor the names of the 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 copyright holders 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.
*/
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.

#ifndef __OPENCV_XIMGPROC_HPP__
#define __OPENCV_XIMGPROC_HPP__
Expand Down Expand Up @@ -68,6 +36,8 @@
/**
@defgroup ximgproc Extended Image Processing
@{
@defgroup ximgproc_binarization Binarization
@defgroup ximgproc_edge Structured forests for fast edge detection
This module contains implementations of modern structured edge detection algorithms,
Expand Down Expand Up @@ -124,7 +94,7 @@ namespace cv
namespace ximgproc
{

//! @addtogroup ximgproc
//! @addtogroup ximgproc_binarization
//! @{

enum ThinningTypes{
Expand Down Expand Up @@ -179,15 +149,20 @@ CV_EXPORTS_W void niBlackThreshold( InputArray _src, OutputArray _dst,
int blockSize, double k, int binarizationMethod = BINARIZATION_NIBLACK,
double r = 128 );

/** @brief Applies a binary blob thinning operation, to achieve a skeletization of the input image.
/** @brief Performs binary image thinning to obtain a skeletonized representation of the input image.
The function transforms a binary blob image into a skeletized form using the technique of Zhang-Suen.
This function applies a thinning algorithm, reducing the binary blobs in the input image to a skeletal form.
By default, it uses the Zhang-Suen technique, which iteratively removes pixels from the boundaries of the blobs
while preserving the overall structure and connectivity of the objects.
@param src Source image: an 8-bit, single-channel binary image where the blobs are represented by pixels with a value of 255 (white),
and the background is 0 (black).
@param dst Destination image of the same size and type as src, where the result of the thinning operation will be stored.
This operation can be performed in-place, meaning `src` and `dst` can be the same.
@param thinningType The thinning algorithm to apply. By default, the Zhang-Suen algorithm is used. See cv::ximgproc::ThinningTypes for other options.
*/
CV_EXPORTS_W void thinning(InputArray src, OutputArray dst, int thinningType = THINNING_ZHANGSUEN);

@param src Source 8-bit single-channel image, containing binary blobs, with blobs having 255 pixel values.
@param dst Destination image of the same size and the same type as src. The function can work in-place.
@param thinningType Value that defines which thinning algorithm should be used. See cv::ximgproc::ThinningTypes
*/
CV_EXPORTS_W void thinning( InputArray src, OutputArray dst, int thinningType = THINNING_ZHANGSUEN);

/** @brief Performs anisotropic diffusion on an image.
Expand Down
135 changes: 39 additions & 96 deletions modules/ximgproc/src/thinning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,117 +92,60 @@ static uint8_t lut_guo_iter1[] = {
1, 1, 1, 1};

// Applies a thinning iteration to a binary image
static void thinningIteration(Mat img, int iter, int thinningType){
Mat marker = Mat::zeros(img.size(), CV_8UC1);
static void thinningIteration(Mat &img, Mat &marker, const uint8_t* const lut) {
int rows = img.rows;
int cols = img.cols;
marker.col(0).setTo(1);
marker.col(cols - 1).setTo(1);
marker.row(0).setTo(1);
marker.row(rows - 1).setTo(1);

if(thinningType == THINNING_ZHANGSUEN){
marker.forEach<uchar>([=](uchar& value, const int postion[]) {
int i = postion[0];
int j = postion[1];
if (i == 0 || j == 0 || i == rows - 1 || j == cols - 1)
return;

auto ptr = img.ptr(i, j); // p1

// p9 p2 p3
// p8 p1 p4
// p7 p6 p5
uchar p2 = ptr[-cols];
uchar p3 = ptr[-cols + 1];
uchar p4 = ptr[1];
uchar p5 = ptr[cols + 1];
uchar p6 = ptr[cols];
uchar p7 = ptr[cols - 1];
uchar p8 = ptr[-1];
uchar p9 = ptr[-cols - 1];

int neighbors = p9 | (p2 << 1) | (p3 << 2) | (p4 << 3) | (p5 << 4) | (p6 << 5) | (p7 << 6) | (p8 << 7);

if (iter == 0)
value = lut_zhang_iter0[neighbors];
else
value = lut_zhang_iter1[neighbors];

//int A = (p2 == 0 && p3 == 1) + (p3 == 0 && p4 == 1) +
// (p4 == 0 && p5 == 1) + (p5 == 0 && p6 == 1) +
// (p6 == 0 && p7 == 1) + (p7 == 0 && p8 == 1) +
// (p8 == 0 && p9 == 1) + (p9 == 0 && p2 == 1);
//int B = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9;
//int m1 = iter == 0 ? (p2 * p4 * p6) : (p2 * p4 * p8);
//int m2 = iter == 0 ? (p4 * p6 * p8) : (p2 * p6 * p8);
//if (A == 1 && (B >= 2 && B <= 6) && m1 == 0 && m2 == 0) value = 0;
// else value = 1;
});
}
if(thinningType == THINNING_GUOHALL){
marker.forEach<uchar>([=](uchar& value, const int postion[]) {
int i = postion[0];
int j = postion[1];
if (i == 0 || j == 0 || i == rows - 1 || j == cols - 1)
return;

auto ptr = img.ptr(i, j); // p1

// p9 p2 p3
// p8 p1 p4
// p7 p6 p5
uchar p2 = ptr[-cols];
uchar p3 = ptr[-cols + 1];
uchar p4 = ptr[1];
uchar p5 = ptr[cols + 1];
uchar p6 = ptr[cols];
uchar p7 = ptr[cols - 1];
uchar p8 = ptr[-1];
uchar p9 = ptr[-cols - 1];

int neighbors = p9 | (p2 << 1) | (p3 << 2) | (p4 << 3) | (p5 << 4) | (p6 << 5) | (p7 << 6) | (p8 << 7);

if (iter == 0)
value = lut_guo_iter0[neighbors];
else
value = lut_guo_iter1[neighbors];

//int C = ((!p2) & (p3 | p4)) + ((!p4) & (p5 | p6)) +
// ((!p6) & (p7 | p8)) + ((!p8) & (p9 | p2));
//int N1 = (p9 | p2) + (p3 | p4) + (p5 | p6) + (p7 | p8);
//int N2 = (p2 | p3) + (p4 | p5) + (p6 | p7) + (p8 | p9);
//int N = N1 < N2 ? N1 : N2;
//int m = iter == 0 ? ((p6 | p7 | (!p9)) & p8) : ((p2 | p3 | (!p5)) & p4);
//if ((C == 1) && ((N >= 2) && ((N <= 3)) & (m == 0))) value = 0;
// else value = 1;
});
}

// Parallelized iteration over pixels excluding the boundary
parallel_for_(Range(1, rows - 1), [&](const Range& range) {
for (int i = range.start; i < range.end; i++) {
const uchar* imgRow = img.ptr(i);
uchar* markerRow = marker.ptr(i);
for (int j = 1; j < cols - 1; j++) {
if (imgRow[j]) {
uchar p2 = imgRow[j - cols] != 0;
uchar p3 = imgRow[j - cols + 1] != 0;
uchar p4 = imgRow[j + 1] != 0;
uchar p5 = imgRow[j + cols + 1] != 0;
uchar p6 = imgRow[j + cols] != 0;
uchar p7 = imgRow[j + cols - 1] != 0;
uchar p8 = imgRow[j - 1] != 0;
uchar p9 = imgRow[j - cols - 1] != 0;

int neighbors = p9 | (p2 << 1) | (p3 << 2) | (p4 << 3) | (p5 << 4) | (p6 << 5) | (p7 << 6) | (p8 << 7);
markerRow[j] = lut[neighbors];
}
}
}
});

img &= marker;
}

// Apply the thinning procedure to a given image
void thinning(InputArray input, OutputArray output, int thinningType){
Mat processed = input.getMat().clone();
CV_CheckTypeEQ(processed.type(), CV_8UC1, "");
// Enforce the range of the input image to be in between 0 - 255
processed /= 255;
Mat input_ = input.getMat();
CV_Assert(!input_.empty());
CV_CheckTypeEQ(input_.type(), CV_8UC1, "");

Mat processed = input_ / 255;
Mat prev = processed.clone();
Mat diff;

Mat marker;
Mat marker_inner = processed(Rect(1, 1, processed.cols - 2, processed.rows - 2));
copyMakeBorder(marker_inner, marker, 1, 1, 1, 1, BORDER_ISOLATED | BORDER_CONSTANT, Scalar(255));

const auto lutIter0 = (thinningType == THINNING_GUOHALL) ? lut_guo_iter0 : lut_zhang_iter0;
const auto lutIter1 = (thinningType == THINNING_GUOHALL) ? lut_guo_iter1 : lut_zhang_iter1;
do {
thinningIteration(processed, 0, thinningType);
thinningIteration(processed, 1, thinningType);
absdiff(processed, prev, diff);
if (!hasNonZero(diff)) break;
thinningIteration(processed, marker, lutIter0);
thinningIteration(processed, marker, lutIter1);
const auto res = cv::norm(processed, prev, cv::NORM_L1);
if (res <= 0) { break; }
processed.copyTo(prev);
}
while (true);
} while (true);

processed *= 255;

output.assign(processed);
}

Expand Down
56 changes: 22 additions & 34 deletions modules/ximgproc/test/test_thinning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,40 @@

namespace opencv_test { namespace {

static int createTestImage(Mat1b& src)
{
src = Mat1b::zeros(Size(256, 256));
// Create a corner point that should not be affected.
src(0, 0) = 255;

for (int x = 50; x < src.cols - 50; x += 50)
{
cv::circle(src, Point(x, x/2), 30 + x/2, Scalar(255), 5);
}
int src_pixels = countNonZero(src);
EXPECT_GT(src_pixels, 0);
return src_pixels;
}

TEST(ximgproc_Thinning, simple_ZHANGSUEN)
{
Mat1b src;
int src_pixels = createTestImage(src);
string dir = cvtest::TS::ptr()->get_data_path();
Mat src = imread(dir + "cv/ximgproc/sources/08.png", IMREAD_GRAYSCALE);
Mat dst,check_img;

Mat1b dst;
thinning(src, dst, THINNING_ZHANGSUEN);
int dst_pixels = countNonZero(dst);
EXPECT_LE(dst_pixels, src_pixels);
EXPECT_EQ(dst(0, 0), 255);

#if 0
imshow("src", src); imshow("dst", dst); waitKey();
#endif
check_img = imread(dir + "cv/ximgproc/results/Thinning_ZHANGSUEN.png", IMREAD_GRAYSCALE);
EXPECT_EQ(0, cvtest::norm(check_img, dst, NORM_INF));

dst = ~src;
thinning(dst, dst, THINNING_ZHANGSUEN);

check_img = imread(dir + "cv/ximgproc/results/Thinning_inv_ZHANGSUEN.png", IMREAD_GRAYSCALE);
EXPECT_EQ(0, cvtest::norm(check_img, dst, NORM_INF));
}

TEST(ximgproc_Thinning, simple_GUOHALL)
{
Mat1b src;
int src_pixels = createTestImage(src);
string dir = cvtest::TS::ptr()->get_data_path();
Mat src = imread(dir + "cv/ximgproc/sources/08.png", IMREAD_GRAYSCALE);
Mat dst,check_img;

Mat1b dst;
thinning(src, dst, THINNING_GUOHALL);
int dst_pixels = countNonZero(dst);
EXPECT_LE(dst_pixels, src_pixels);
EXPECT_EQ(dst(0, 0), 255);

#if 0
imshow("src", src); imshow("dst", dst); waitKey();
#endif
}
check_img = imread(dir + "cv/ximgproc/results/Thinning_GUOHALL.png", IMREAD_GRAYSCALE);
EXPECT_EQ(0, cvtest::norm(check_img, dst, NORM_INF));

dst = ~src;
thinning(dst, dst, THINNING_GUOHALL);

check_img = imread(dir + "cv/ximgproc/results/Thinning_inv_GUOHALL.png", IMREAD_GRAYSCALE);
EXPECT_EQ(0, cvtest::norm(check_img, dst, NORM_INF));
}

}} // namespace

0 comments on commit 66cf9b3

Please sign in to comment.