Commit 0f5d6ae1 authored by Dietrich Büsching's avatar Dietrich Büsching Committed by Alexander Alekhin

Merge pull request #1672 from dbuesching:rl_morphology

* run length morphology

* remove unused code, avoid warnings for undefined functions

* handle empty input in getBoundingRectangle correctly, remove unused operations

* changes according to code review
parent 729737dd
......@@ -15,3 +15,4 @@ Extended Image Processing
- Deriche Filter
- Pei&Lin Normalization
- Ridge Detection Filter
- Binary morphology on run-length encoded images
......@@ -288,3 +288,13 @@
year={1977},
publisher={IEEE Computer Society}
}
@inproceedings{Breuel2008,
title = {Binary Morphology and Related Operations on Run-Length Representations.},
author = {Breuel, Thomas},
year = {2008},
month = {01},
pages = {159-166},
volume = {1},
booktitle = {VISAPP 2008 - 3rd International Conference on Computer Vision Theory and Applications, Proceedings}
}
......@@ -56,6 +56,7 @@
#include "ximgproc/fourier_descriptors.hpp"
#include "ximgproc/ridgefilter.hpp"
#include "ximgproc/brightedges.hpp"
#include "ximgproc/run_length_morphology.hpp"
/** @defgroup ximgproc Extended Image Processing
......@@ -76,6 +77,26 @@ i.e. algorithms which somehow takes into account pixel affinities in natural ima
@defgroup ximgproc_fast_line_detector Fast line detector
@defgroup ximgproc_fourier Fourier descriptors
@defgroup ximgproc_run_length_morphology Binary morphology on run-length encoded image
These functions support morphological operations on binary images. In order to be fast and space efficient binary images are encoded with a run-length representation.
This representation groups continuous horizontal sequences of "on" pixels together in a "run". A run is charactarized by the column position of the first pixel in the run, the column
position of the last pixel in the run and the row position. This representation is very compact for binary images which contain large continuous areas of "on" and "off" pixels. A checkerboard
pattern would be a good example. The representation is not so suitable for binary images created from random noise images or other images where little correlation between neighboring pixels
exists.
The morphological operations supported here are very similar to the operations supported in the imgproc module. In general they are fast. However on several occasions they are slower than the functions
from imgproc. The structuring elements of cv::MORPH_RECT and cv::MORPH_CROSS have very good support from the imgproc module. Also small structuring elements are very fast in imgproc (presumably
due to opencl support). Therefore the functions from this module are recommended for larger structuring elements (cv::MORPH_ELLIPSE or self defined structuring elements). A sample application
(run_length_morphology_demo) is supplied which allows to compare the speed of some morphological operations for the functions using run-length encoding and the imgproc functions for a given image.
Run length encoded images are stored in standard opencv images. Images have a single column of cv::Point3i elements. The number of rows is the number of run + 1. The first row contains
the size of the original (not encoded) image. For the runs the following mapping is used (x: column begin, y: column end (last column), z: row).
The size of the original image is required for compatiblity with the imgproc functions when the boundary handling requires that pixel outside the image boundary are
"on".
@}
*/
......
// 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_RUN_LENGTH_MORPHOLOGY_HPP__
#define __OPENCV_RUN_LENGTH_MORPHOLOGY_HPP__
#include <opencv2/core.hpp>
namespace cv {
namespace ximgproc {
namespace rl {
//! @addtogroup ximgproc_run_length_morphology
//! @{
/**
* @brief Applies a fixed-level threshold to each array element.
*
*
* @param src input array (single-channel).
* @param rlDest resulting run length encoded image.
* @param thresh threshold value.
* @param type thresholding type (only cv::THRESH_BINARY and cv::THRESH_BINARY_INV are supported)
*
*/
CV_EXPORTS void threshold(InputArray src, OutputArray rlDest, double thresh, int type);
/**
* @brief Dilates an run-length encoded binary image by using a specific structuring element.
*
*
* @param rlSrc input image
* @param rlDest result
* @param rlKernel kernel
* @param anchor position of the anchor within the element; default value (0, 0)
* is usually the element center.
*
*/
CV_EXPORTS void dilate(InputArray rlSrc, OutputArray rlDest, InputArray rlKernel, Point anchor = Point(0, 0));
/**
* @brief Erodes an run-length encoded binary image by using a specific structuring element.
*
*
* @param rlSrc input image
* @param rlDest result
* @param rlKernel kernel
* @param bBoundaryOn indicates whether pixel outside the image boundary are assumed to be on
(True: works in the same way as the default of cv::erode, False: is a little faster)
* @param anchor position of the anchor within the element; default value (0, 0)
* is usually the element center.
*
*/
CV_EXPORTS void erode(InputArray rlSrc, OutputArray rlDest, InputArray rlKernel,
bool bBoundaryOn = true, Point anchor = Point(0, 0));
/**
* @brief Returns a run length encoded structuring element of the specified size and shape.
*
*
* @param shape Element shape that can be one of cv::MorphShapes
* @param ksize Size of the structuring element.
*
*/
CV_EXPORTS cv::Mat getStructuringElement(int shape, Size ksize);
/**
* @brief Paint run length encoded binary image into an image.
*
*
* @param image image to paint into (currently only single channel images).
* @param rlSrc run length encoded image
* @param value all foreground pixel of the binary image are set to this value
*
*/
CV_EXPORTS void paint(InputOutputArray image, InputArray rlSrc, const cv::Scalar& value);
/**
* @brief Check whether a custom made structuring element can be used with run length morphological operations.
* (It must consist of a continuous array of single runs per row)
*
* @param rlStructuringElement mask to be tested
*/
CV_EXPORTS bool isRLMorphologyPossible(InputArray rlStructuringElement);
/**
* @brief Creates a run-length encoded image from a vector of runs (column begin, column end, row)
*
* @param runs vector of runs
* @param res result
* @param size image size (to be used if an "on" boundary should be used in erosion, using the default
* means that the size is computed from the extension of the input)
*/
CV_EXPORTS void createRLEImage(std::vector<cv::Point3i>& runs, OutputArray res, Size size = Size(0, 0));
/**
* @brief Applies a morphological operation to a run-length encoded binary image.
*
*
* @param rlSrc input image
* @param rlDest result
* @param op all operations supported by cv::morphologyEx (except cv::MORPH_HITMISS)
* @param rlKernel kernel
* @param bBoundaryOnForErosion indicates whether pixel outside the image boundary are assumed
* to be on for erosion operations (True: works in the same way as the default of cv::erode,
* False: is a little faster)
* @param anchor position of the anchor within the element; default value (0, 0) is usually the element center.
*
*/
CV_EXPORTS void morphologyEx(InputArray rlSrc, OutputArray rlDest, int op, InputArray rlKernel,
bool bBoundaryOnForErosion = true, Point anchor = Point(0,0));
}
}
}
#endif
// 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.
#include "perf_precomp.hpp"
namespace opencv_test {
namespace {
typedef tuple<int, Size, int> RLParams;
typedef TestBaseWithParam<RLParams> RLMorphologyPerfTest;
PERF_TEST_P(RLMorphologyPerfTest, perf, Combine(Values(1,7, 21), Values(sz720p, sz2160p),
Values(MORPH_ERODE, MORPH_DILATE, MORPH_OPEN, MORPH_CLOSE, MORPH_GRADIENT,MORPH_TOPHAT, MORPH_BLACKHAT)))
{
RLParams params = GetParam();
int seSize = get<0>(params);
Size sz = get<1>(params);
int op = get<2>(params);
Mat src(sz, CV_8U);
Mat thresholded, dstRLE;
Mat se = rl::getStructuringElement(MORPH_ELLIPSE, cv::Size(2 * seSize + 1, 2 * seSize + 1));
declare.in(src, WARMUP_RNG);
TEST_CYCLE_N(4)
{
rl::threshold(src, thresholded, 100.0, THRESH_BINARY);
rl::morphologyEx(thresholded, dstRLE, op, se);
}
SANITY_CHECK_NOTHING();
}
}
} // namespace
\ No newline at end of file
#include <iostream>
#include "opencv2/imgproc.hpp"
#include "opencv2/ximgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
using namespace std;
using namespace cv;
using namespace cv::ximgproc;
// Adapted from cv_timer in cv_utilities
class Timer
{
public:
Timer() : start_(0), time_(0) {}
void start()
{
start_ = cv::getTickCount();
}
void stop()
{
CV_Assert(start_ != 0);
int64 end = cv::getTickCount();
time_ += end - start_;
start_ = 0;
}
double time()
{
double ret = time_ / cv::getTickFrequency();
time_ = 0;
return ret;
}
private:
int64 start_, time_;
};
static void help()
{
printf("\nAllows to estimate the efficiency of the morphology operations implemented\n"
"in ximgproc/run_length_morphology.cpp\n"
"Call:\n example_ximgproc_run_length_morphology_demo [image] -u=factor_upscaling image\n"
"Similar to the morphology2 sample of the main opencv library it shows the use\n"
"of rect, ellipse and cross kernels\n\n"
"As rectangular and cross-shaped structuring elements are highly optimized in opencv_imgproc module,\n"
"only with elliptical structuring elements a speedup is possible (e.g. for larger circles).\n"
"Run-length morphology has advantages for larger images.\n"
"You can verify this by upscaling your input with e.g. -u=2\n");
printf( "Hot keys: \n"
"\tESC - quit the program\n"
"\tr - use rectangle structuring element\n"
"\te - use elliptic structuring element\n"
"\tc - use cross-shaped structuring element\n"
"\tSPACE - loop through all the options\n" );
}
static void print_introduction()
{
printf("\nFirst select a threshold for binarization.\n"
"Then move the sliders for erosion/dilation or open/close operation\n\n"
"The ratio between the time of the execution from opencv_imgproc\n"
"and the code using run-length encoding will be displayed in the console\n\n");
}
Mat src, dst;
int element_shape = MORPH_ELLIPSE;
//the address of variable which receives trackbar position update
int max_size = 40;
int open_close_pos = 0;
int erode_dilate_pos = 0;
int nThreshold = 100;
cv::Mat binaryImage;
cv::Mat binaryRLE, dstRLE;
cv::Mat rlePainted;
static void PaintRLEToImage(cv::Mat& rleImage, cv::Mat& res, unsigned char uValue)
{
res = cv::Scalar(0);
rl::paint(res, rleImage, Scalar((double) uValue));
}
static bool AreImagesIdentical(cv::Mat& image1, cv::Mat& image2)
{
cv::Mat diff;
cv::absdiff(image1, image2, diff);
int nDiff = cv::countNonZero(diff);
return (nDiff == 0);
}
// callback function for open/close trackbar
static void OpenClose(int, void*)
{
int n = open_close_pos - max_size;
int an = n > 0 ? n : -n;
Mat element = getStructuringElement(element_shape, Size(an*2+1, an*2+1), Point(an, an) );
Timer timer;
timer.start();
if( n < 0 )
morphologyEx(binaryImage, dst, MORPH_OPEN, element);
else
morphologyEx(binaryImage, dst, MORPH_CLOSE, element);
timer.stop();
double imgproc_duration = timer.time();
element = rl::getStructuringElement(element_shape, Size(an * 2 + 1, an * 2 + 1));
Timer timer2;
timer2.start();
if (n < 0)
rl::morphologyEx(binaryRLE, dstRLE, MORPH_OPEN, element, true);
else
rl::morphologyEx(binaryRLE, dstRLE, MORPH_CLOSE, element, true);
timer2.stop();
double rl_duration = timer2.time();
cout << "ratio open/close duration: " << rl_duration / imgproc_duration << " (run-length: "
<< rl_duration << ", pixelwise: " << imgproc_duration << " )" << std::endl;
PaintRLEToImage(dstRLE, rlePainted, (unsigned char)255);
if (!AreImagesIdentical(dst, rlePainted))
{
cout << "error result image are not identical" << endl;
}
imshow("Open/Close", rlePainted);
}
// callback function for erode/dilate trackbar
static void ErodeDilate(int, void*)
{
int n = erode_dilate_pos - max_size;
int an = n > 0 ? n : -n;
Mat element = getStructuringElement(element_shape, Size(an*2+1, an*2+1), Point(an, an) );
Timer timer;
timer.start();
if( n < 0 )
erode(binaryImage, dst, element);
else
dilate(binaryImage, dst, element);
timer.stop();
double imgproc_duration = timer.time();
element = rl::getStructuringElement(element_shape, Size(an*2+1, an*2+1));
Timer timer2;
timer2.start();
if( n < 0 )
rl::erode(binaryRLE, dstRLE, element, true);
else
rl::dilate(binaryRLE, dstRLE, element);
timer2.stop();
double rl_duration = timer2.time();
PaintRLEToImage(dstRLE, rlePainted, (unsigned char)255);
cout << "ratio erode/dilate duration: " << rl_duration / imgproc_duration <<
" (run-length: " << rl_duration << ", pixelwise: " << imgproc_duration << " )" << std::endl;
if (!AreImagesIdentical(dst, rlePainted))
{
cout << "error result image are not identical" << endl;
}
imshow("Erode/Dilate", rlePainted);
}
static void OnChangeThreshold(int, void*)
{
threshold(src, binaryImage, (double) nThreshold, 255.0, THRESH_BINARY );
rl::threshold(src, binaryRLE, (double) nThreshold, THRESH_BINARY);
imshow("Threshold", binaryImage);
}
int main( int argc, char** argv )
{
cv::CommandLineParser parser(argc, argv, "{help h||}{ @image | ../data/aloeL.jpg | }{u| |}");
if (parser.has("help"))
{
help();
return 0;
}
std::string filename = parser.get<std::string>("@image");
cv::Mat srcIn;
if( (srcIn = imread(filename,IMREAD_GRAYSCALE)).empty() )
{
help();
return -1;
}
int nScale = 1;
if (parser.has("u"))
{
int theScale = parser.get<int>("u");
if (theScale > 1)
nScale = theScale;
}
if (nScale == 1)
src = srcIn;
else
cv::resize(srcIn, src, cv::Size(srcIn.rows * nScale, srcIn.cols * nScale));
cout << "scale factor read " << nScale << endl;
print_introduction();
//create windows for output images
namedWindow("Open/Close",1);
namedWindow("Erode/Dilate",1);
namedWindow("Threshold",1);
open_close_pos = erode_dilate_pos = max_size - 10;
createTrackbar("size s.e.", "Open/Close",&open_close_pos,max_size*2+1,OpenClose);
createTrackbar("size s.e.", "Erode/Dilate",&erode_dilate_pos,max_size*2+1,ErodeDilate);
createTrackbar("threshold", "Threshold",&nThreshold,255, OnChangeThreshold);
OnChangeThreshold(0, 0);
rlePainted.create(cv::Size(src.cols, src.rows), CV_8UC1);
for(;;)
{
OpenClose(open_close_pos, 0);
ErodeDilate(erode_dilate_pos, 0);
char c = (char)waitKey(0);
if( c == 27 )
break;
if( c == 'e' )
element_shape = MORPH_ELLIPSE;
else if( c == 'r' )
element_shape = MORPH_RECT;
else if( c == 'c' )
element_shape = MORPH_CROSS;
else if( c == ' ' )
element_shape = (element_shape + 1) % 3;
}
return 0;
}
\ No newline at end of file
This diff is collapsed.
// 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.
#include "test_precomp.hpp"
#include "opencv2/ximgproc/run_length_morphology.hpp"
#include "opencv2/imgproc.hpp"
namespace opencv_test {
namespace {
const Size img_size(640, 480);
const int tile_size(20);
typedef tuple<cv::MorphTypes, int, int> RLMParams;
typedef tuple<cv::MorphTypes, int, int, int> RLMSParams;
class RLTestBase
{
public:
RLTestBase() { }
protected:
std::vector<Mat> test_image;
std::vector<Mat> test_image_rle;
void generateCheckerBoard(Mat& image);
void generateRandomImage(Mat& image);
bool areImagesIdentical(Mat& pixelImage, Mat& rleImage);
bool arePixelImagesIdentical(Mat& image1, Mat& image2);
void setUp_impl();
};
void RLTestBase::generateCheckerBoard(Mat& image)
{
image.create(img_size, CV_8UC1);
for (int iy = 0; iy < img_size.height; iy += tile_size)
{
Range rowRange(iy, std::min(iy + tile_size, img_size.height));
for (int ix = 0; ix < img_size.width; ix += tile_size)
{
Range colRange(ix, std::min(ix + tile_size, img_size.width));
Mat tile(image, rowRange, colRange);
bool bBright = ((iy + ix) % (2 * tile_size) == 0);
tile = (bBright ? Scalar(255) : Scalar(0));
}
}
}
void RLTestBase::generateRandomImage(Mat& image)
{
image.create(img_size, CV_8UC1);
randu(image, Scalar::all(0), Scalar::all(255));
}
void RLTestBase::setUp_impl()
{
test_image.resize(2);
test_image_rle.resize(2);
generateCheckerBoard(test_image[0]);
rl::threshold(test_image[0], test_image_rle[0], 100.0, THRESH_BINARY);
cv::Mat theRandom;
generateRandomImage(theRandom);
double dThreshold = 254.0;
cv::threshold(theRandom, test_image[1], dThreshold, 255.0, THRESH_BINARY);
rl::threshold(theRandom, test_image_rle[1], dThreshold, THRESH_BINARY);
}
bool RLTestBase::areImagesIdentical(Mat& pixelImage, Mat& rleImage)
{
cv::Mat rleConverted;
rleConverted = cv::Mat::zeros(pixelImage.rows, pixelImage.cols, CV_8UC1);
rl::paint(rleConverted, rleImage, Scalar(255.0));
return arePixelImagesIdentical(pixelImage, rleConverted);
}
bool RLTestBase::arePixelImagesIdentical(Mat& image1, Mat& image2)
{
cv::Mat diff;
cv::absdiff(image1, image2, diff);
int nDiff = cv::countNonZero(diff);
return (nDiff == 0);
}
class RL_Identical_Result_Simple : public RLTestBase, public ::testing::TestWithParam<RLMSParams>
{
public:
RL_Identical_Result_Simple() { }
protected:
virtual void SetUp() { setUp_impl(); }
};
TEST_P(RL_Identical_Result_Simple, simple)
{
Mat resPix, resRLE;
RLMSParams param = GetParam();
cv::MorphTypes elementType = get<0>(param);
int nSize = get<1>(param);
int image = get<2>(param);
int op = get<3>(param);
Mat element = getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1),
Point(nSize, nSize));
morphologyEx(test_image[image], resPix, op, element);
Mat elementRLE = rl::getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1));
rl::morphologyEx(test_image_rle[image], resRLE, op, elementRLE);
ASSERT_TRUE(areImagesIdentical(resPix, resRLE));
}
INSTANTIATE_TEST_CASE_P(TypicalSET, RL_Identical_Result_Simple, Combine(Values(MORPH_RECT, MORPH_CROSS, MORPH_ELLIPSE),
Values(1, 5, 11), Values(0, 1), Values(MORPH_ERODE, MORPH_DILATE, MORPH_OPEN, MORPH_CLOSE, MORPH_GRADIENT, MORPH_TOPHAT, MORPH_BLACKHAT)));
class RL_Identical_Result : public RLTestBase, public ::testing::TestWithParam<RLMParams>
{
public:
RL_Identical_Result() { }
protected:
virtual void SetUp() { setUp_impl(); }
};
TEST_P(RL_Identical_Result, erosion_no_boundary)
{
Mat resPix, resRLE;
RLMParams param = GetParam();
cv::MorphTypes elementType = get<0>(param);
int nSize = get<1>(param);
int image = get<2>(param);
Mat element = getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1),
Point(nSize, nSize));
erode(test_image[image], resPix, element, cv::Point(-1,-1), 1, BORDER_CONSTANT, cv::Scalar(0));
Mat elementRLE = rl::getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1));
rl::erode(test_image_rle[image], resRLE, elementRLE, false);
ASSERT_TRUE(areImagesIdentical(resPix, resRLE));
}
TEST_P(RL_Identical_Result, erosion_with_offset)
{
Mat resPix, resRLE;
RLMParams param = GetParam();
cv::MorphTypes elementType = get<0>(param);
int nSize = get<1>(param);
int image = get<2>(param);
int nOffset = nSize - 1;
Mat element = getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1),
Point(nSize, nSize));
erode(test_image[image], resPix, element, cv::Point(nSize + nOffset, nSize + nOffset));
Mat elementRLE = rl::getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1));
rl::erode(test_image_rle[image], resRLE, elementRLE, true, Point(nOffset, nOffset));
ASSERT_TRUE(areImagesIdentical(resPix, resRLE));
}
TEST_P(RL_Identical_Result, dilation_with_offset)
{
Mat resPix, resRLE;
RLMParams param = GetParam();
cv::MorphTypes elementType = get<0>(param);
int nSize = get<1>(param);
int image = get<2>(param);
int nOffset = nSize - 1;
Mat element = getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1),
Point(nSize, nSize));
dilate(test_image[image], resPix, element, cv::Point(nSize + nOffset, nSize + nOffset));
Mat elementRLE = rl::getStructuringElement(elementType, Size(nSize * 2 + 1, nSize * 2 + 1));
rl::dilate(test_image_rle[image], resRLE, elementRLE, Point(nOffset, nOffset));
ASSERT_TRUE(areImagesIdentical(resPix, resRLE));
}
INSTANTIATE_TEST_CASE_P(TypicalSET, RL_Identical_Result, Combine(Values(MORPH_RECT, MORPH_CROSS, MORPH_ELLIPSE), Values(1,5,11), Values(0,1)));
class RL_CreateCustomKernel : public RLTestBase, public testing::Test
{
public:
RL_CreateCustomKernel() { }
protected:
virtual void SetUp() { setUp_impl(); }
};
TEST_F(RL_CreateCustomKernel, check_valid)
{
// create a diamond
int nSize = 21;
std::vector<Point3i> runs;
for (int i = 0; i < nSize; ++i)
{
runs.emplace_back(Point3i(-i, i, -nSize + i));
runs.emplace_back(Point3i(-i, i, nSize - i));
}
runs.emplace_back(Point3i(-nSize, nSize, 0));
Mat kernel, dest;
rl::createRLEImage(runs, kernel);
ASSERT_TRUE(rl::isRLMorphologyPossible(kernel));
rl::erode(test_image_rle[0], dest, kernel);
//only one row means: no runs, all pixels off
ASSERT_TRUE(dest.rows == 1);
}
typedef tuple<int> RLPParams;
class RL_Paint : public RLTestBase, public ::testing::TestWithParam<RLPParams>
{
public:
RL_Paint() { }
protected:
virtual void SetUp() { setUp_impl(); }
};
TEST_P(RL_Paint, same_result)
{
Mat converted, pixBinary, painted;
RLPParams param = GetParam();
int rType = get<0>(param);
double dThreshold = 100.0;
double dMaxValue = 105.0;
test_image[1].convertTo(converted, rType);
cv::threshold(converted, pixBinary, dThreshold, dMaxValue, THRESH_BINARY);
painted.create(test_image[1].rows, test_image[1].cols, rType);
painted = cv::Scalar(0.0);
rl::paint(painted, test_image_rle[1], Scalar(dMaxValue));
ASSERT_TRUE(arePixelImagesIdentical(pixBinary, painted));
}
INSTANTIATE_TEST_CASE_P(TypicalSET, RL_Paint, Values(CV_8U, CV_16U, CV_16S, CV_32F, CV_64F));
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment