Commit 9a4265a8 authored by Anatoly Baksheev's avatar Anatoly Baksheev

fast nlm (class version)

parent 4b5bbb77
...@@ -849,15 +849,15 @@ gpu::nonLocalMeans ...@@ -849,15 +849,15 @@ gpu::nonLocalMeans
------------------- -------------------
Performs pure non local means denoising without any simplification, and thus it is not fast. Performs pure non local means denoising without any simplification, and thus it is not fast.
.. ocv:function:: void nonLocalMeans(const GpuMat& src, GpuMat& dst, float h, int search_widow_size = 11, int block_size = 7, int borderMode = BORDER_DEFAULT, Stream& s = Stream::Null()) .. ocv:function:: void nonLocalMeans(const GpuMat& src, GpuMat& dst, float h, int search_window = 21, int block_size = 7, int borderMode = BORDER_DEFAULT, Stream& s = Stream::Null())
:param src: Source image. Supports only CV_8UC1, CV_8UC2 and CV_8UC3. :param src: Source image. Supports only CV_8UC1, CV_8UC2 and CV_8UC3.
:param dst: Destination imagwe. :param dst: Destination image.
:param h: Filter sigma regulating filter strength for color. :param h: Filter sigma regulating filter strength for color.
:param search_widow_size: Size of search window. :param search_window: Size of search window.
:param block_size: Size of block used for computing weights. :param block_size: Size of block used for computing weights.
...@@ -869,6 +869,72 @@ Performs pure non local means denoising without any simplification, and thus it ...@@ -869,6 +869,72 @@ Performs pure non local means denoising without any simplification, and thus it
:ocv:func:`fastNlMeansDenoising` :ocv:func:`fastNlMeansDenoising`
gpu::FastNonLocalMeansDenoising
-------------------------------
.. ocv:class:: gpu::FastNonLocalMeansDenoising
class FastNonLocalMeansDenoising
{
public:
//! Simple method, recommended for grayscale images (though it supports multichannel images)
void simpleMethod(const GpuMat& src, GpuMat& dst, float h, int search_window = 21, int block_size = 7, Stream& s = Stream::Null());
//! Processes luminance and color components separatelly
void labMethod(const GpuMat& src, GpuMat& dst, float h_luminance, float h_color, int search_window = 21, int block_size = 7, Stream& s = Stream::Null());
};
The class implements fast approximate Non Local Means Denoising algorithm.
gpu::FastNonLocalMeansDenoising::simpleMethod()
-------------------------------------
Perform image denoising using Non-local Means Denoising algorithm http://www.ipol.im/pub/algo/bcm_non_local_means_denoising with several computational optimizations. Noise expected to be a gaussian white noise
.. ocv:function:: void gpu::FastNonLocalMeansDenoising::simpleMethod(const GpuMat& src, GpuMat& dst, float h, int search_window = 21, int block_size = 7, Stream& s = Stream::Null());
:param src: Input 8-bit 1-channel, 2-channel or 3-channel image.
:param dst: Output image with the same size and type as ``src`` .
:param h: Parameter regulating filter strength. Big h value perfectly removes noise but also removes image details, smaller h value preserves details but also preserves some noise
:param search_window: Size in pixels of the window that is used to compute weighted average for given pixel. Should be odd. Affect performance linearly: greater search_window - greater denoising time. Recommended value 21 pixels
:param block_size: Size in pixels of the template patch that is used to compute weights. Should be odd. Recommended value 7 pixels
:param stream: Stream for the asynchronous invocations.
This function expected to be applied to grayscale images. For colored images look at ``FastNonLocalMeansDenoising::labMethod``.
.. seealso::
:ocv:func:`fastNlMeansDenoising`
gpu::FastNonLocalMeansDenoising::labMethod()
-------------------------------------
Modification of ``FastNonLocalMeansDenoising::simpleMethod`` for color images
.. ocv:function:: void gpu::FastNonLocalMeansDenoising::labMethod(const GpuMat& src, GpuMat& dst, float h_luminance, float h_color, int search_window = 21, int block_size = 7, Stream& s = Stream::Null());
:param src: Input 8-bit 3-channel image.
:param dst: Output image with the same size and type as ``src`` .
:param h_luminance: Parameter regulating filter strength. Big h value perfectly removes noise but also removes image details, smaller h value preserves details but also preserves some noise
:param float: The same as h but for color components. For most images value equals 10 will be enought to remove colored noise and do not distort colors
:param search_window: Size in pixels of the window that is used to compute weighted average for given pixel. Should be odd. Affect performance linearly: greater search_window - greater denoising time. Recommended value 21 pixels
:param block_size: Size in pixels of the template patch that is used to compute weights. Should be odd. Recommended value 7 pixels
:param stream: Stream for the asynchronous invocations.
The function converts image to CIELAB colorspace and then separately denoise L and AB components with given h parameters using ``FastNonLocalMeansDenoising::simpleMethod`` function.
.. seealso::
:ocv:func:`fastNlMeansDenoisingColored`
gpu::alphaComp gpu::alphaComp
------------------- -------------------
Composites two images using alpha opacity values contained in each image. Composites two images using alpha opacity values contained in each image.
......
...@@ -774,11 +774,24 @@ CV_EXPORTS void bilateralFilter(const GpuMat& src, GpuMat& dst, int kernel_size, ...@@ -774,11 +774,24 @@ CV_EXPORTS void bilateralFilter(const GpuMat& src, GpuMat& dst, int kernel_size,
int borderMode = BORDER_DEFAULT, Stream& stream = Stream::Null()); int borderMode = BORDER_DEFAULT, Stream& stream = Stream::Null());
//! Brute force non-local means algorith (slow but universal) //! Brute force non-local means algorith (slow but universal)
CV_EXPORTS void nonLocalMeans(const GpuMat& src, GpuMat& dst, float h, CV_EXPORTS void nonLocalMeans(const GpuMat& src, GpuMat& dst, float h, int search_window = 21, int block_size = 7, int borderMode = BORDER_DEFAULT, Stream& s = Stream::Null());
int search_widow_size = 11, int block_size = 7, int borderMode = BORDER_DEFAULT, Stream& s = Stream::Null());
//! Fast (but approximate)version of non-local means algorith similar to CPU function (running sums technique) //! Fast (but approximate)version of non-local means algorith similar to CPU function (running sums technique)
CV_EXPORTS void fastNlMeansDenoising( const GpuMat& src, GpuMat& dst, float h, int search_radius = 10, int block_radius = 3, Stream& s = Stream::Null()); class CV_EXPORTS FastNonLocalMeansDenoising
{
public:
//! Simple method, recommended for grayscale images (though it supports multichannel images)
void simpleMethod(const GpuMat& src, GpuMat& dst, float h, int search_window = 21, int block_size = 7, Stream& s = Stream::Null());
//! Processes luminance and color components separatelly
void labMethod(const GpuMat& src, GpuMat& dst, float h_luminance, float h_color, int search_window = 21, int block_size = 7, Stream& s = Stream::Null());
private:
GpuMat buffer, extended_src_buffer;
GpuMat lab, l, ab;
};
struct CV_EXPORTS CannyBuf; struct CV_EXPORTS CannyBuf;
......
...@@ -3,16 +3,18 @@ ...@@ -3,16 +3,18 @@
using namespace std; using namespace std;
using namespace testing; using namespace testing;
#define GPU_DENOISING_IMAGE_SIZES testing::Values(perf::szVGA, perf::szXGA, perf::sz720p, perf::sz1080p)
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// BilateralFilter // BilateralFilter
DEF_PARAM_TEST(Sz_Depth_Cn_KernelSz, cv::Size, MatDepth , int, int); DEF_PARAM_TEST(Sz_Depth_Cn_KernelSz, cv::Size, MatDepth, int, int);
PERF_TEST_P(Sz_Depth_Cn_KernelSz, Denoising_BilateralFilter, PERF_TEST_P(Sz_Depth_Cn_KernelSz, Denoising_BilateralFilter,
Combine(GPU_TYPICAL_MAT_SIZES, Values(CV_8U, CV_16U, CV_32F), GPU_CHANNELS_1_3_4, Values(3, 5, 9))) Combine(GPU_DENOISING_IMAGE_SIZES, Values(CV_8U, CV_32F), testing::Values(1, 3), Values(3, 5, 9)))
{ {
declare.time(30.0); declare.time(60.0);
cv::Size size = GET_PARAM(0); cv::Size size = GET_PARAM(0);
int depth = GET_PARAM(1); int depth = GET_PARAM(1);
...@@ -57,12 +59,12 @@ PERF_TEST_P(Sz_Depth_Cn_KernelSz, Denoising_BilateralFilter, ...@@ -57,12 +59,12 @@ PERF_TEST_P(Sz_Depth_Cn_KernelSz, Denoising_BilateralFilter,
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// nonLocalMeans // nonLocalMeans
DEF_PARAM_TEST(Sz_Depth_Cn_WinSz_BlockSz, cv::Size, MatDepth , int, int, int); DEF_PARAM_TEST(Sz_Depth_Cn_WinSz_BlockSz, cv::Size, MatDepth, int, int, int);
PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_NonLocalMeans, PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_NonLocalMeans,
Combine(GPU_TYPICAL_MAT_SIZES, Values<MatDepth>(CV_8U), Values(1), Values(21), Values(5, 7))) Combine(GPU_DENOISING_IMAGE_SIZES, Values<MatDepth>(CV_8U), Values(1, 3), Values(21), Values(5, 7)))
{ {
declare.time(30.0); declare.time(60.0);
cv::Size size = GET_PARAM(0); cv::Size size = GET_PARAM(0);
int depth = GET_PARAM(1); int depth = GET_PARAM(1);
...@@ -101,22 +103,21 @@ PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_NonLocalMeans, ...@@ -101,22 +103,21 @@ PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_NonLocalMeans,
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// fastNonLocalMeans // fastNonLocalMeans
DEF_PARAM_TEST(Sz_Depth_Cn_WinSz_BlockSz, cv::Size, MatDepth , int, int, int); DEF_PARAM_TEST(Sz_Depth_WinSz_BlockSz, cv::Size, MatDepth, int, int);
PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_FastNonLocalMeans, PERF_TEST_P(Sz_Depth_WinSz_BlockSz, Denoising_FastNonLocalMeans,
Combine(GPU_TYPICAL_MAT_SIZES, Values<MatDepth>(CV_8U), Values(1), Values(21), Values(5, 7))) Combine(GPU_DENOISING_IMAGE_SIZES, Values<MatDepth>(CV_8U), Values(21), Values(7)))
{ {
declare.time(30.0); declare.time(150.0);
cv::Size size = GET_PARAM(0); cv::Size size = GET_PARAM(0);
int depth = GET_PARAM(1); int depth = GET_PARAM(1);
int channels = GET_PARAM(2);
int search_widow_size = GET_PARAM(3); int search_widow_size = GET_PARAM(2);
int block_size = GET_PARAM(4); int block_size = GET_PARAM(3);
float h = 10; float h = 10;
int type = CV_MAKE_TYPE(depth, channels); int type = CV_MAKE_TYPE(depth, 1);
cv::Mat src(size, type); cv::Mat src(size, type);
fillRandom(src); fillRandom(src);
...@@ -125,11 +126,13 @@ PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_FastNonLocalMeans, ...@@ -125,11 +126,13 @@ PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_FastNonLocalMeans,
{ {
cv::gpu::GpuMat d_src(src); cv::gpu::GpuMat d_src(src);
cv::gpu::GpuMat d_dst; cv::gpu::GpuMat d_dst;
cv::gpu::fastNlMeansDenoising(d_src, d_dst, h, search_widow_size/2, block_size/2); cv::gpu::FastNonLocalMeansDenoising fnlmd;
fnlmd.simpleMethod(d_src, d_dst, h, search_widow_size, block_size);
TEST_CYCLE() TEST_CYCLE()
{ {
cv::gpu::fastNlMeansDenoising(d_src, d_dst, h, search_widow_size/2, block_size/2); fnlmd.simpleMethod(d_src, d_dst, h, search_widow_size, block_size);
} }
} }
else else
...@@ -143,3 +146,49 @@ PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_FastNonLocalMeans, ...@@ -143,3 +146,49 @@ PERF_TEST_P(Sz_Depth_Cn_WinSz_BlockSz, Denoising_FastNonLocalMeans,
} }
} }
} }
//////////////////////////////////////////////////////////////////////
// fastNonLocalMeans (colored)
PERF_TEST_P(Sz_Depth_WinSz_BlockSz, Denoising_FastNonLocalMeansColored,
Combine(GPU_DENOISING_IMAGE_SIZES, Values<MatDepth>(CV_8U), Values(21), Values(7)))
{
declare.time(350.0);
cv::Size size = GET_PARAM(0);
int depth = GET_PARAM(1);
int search_widow_size = GET_PARAM(2);
int block_size = GET_PARAM(3);
float h = 10;
int type = CV_MAKE_TYPE(depth, 3);
cv::Mat src(size, type);
fillRandom(src);
if (runOnGpu)
{
cv::gpu::GpuMat d_src(src);
cv::gpu::GpuMat d_dst;
cv::gpu::FastNonLocalMeansDenoising fnlmd;
fnlmd.labMethod(d_src, d_dst, h, h, search_widow_size, block_size);
TEST_CYCLE()
{
fnlmd.labMethod(d_src, d_dst, h, h, search_widow_size, block_size);
}
}
else
{
cv::Mat dst;
cv::fastNlMeansDenoisingColored(src, dst, h, h, block_size, search_widow_size);
TEST_CYCLE()
{
cv::fastNlMeansDenoisingColored(src, dst, h, h, block_size, search_widow_size);
}
}
}
\ No newline at end of file
...@@ -97,7 +97,7 @@ namespace cv { namespace gpu { namespace device ...@@ -97,7 +97,7 @@ namespace cv { namespace gpu { namespace device
} }
template void copyMakeBorder_gpu<uchar, 1>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream); template void copyMakeBorder_gpu<uchar, 1>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream);
//template void copyMakeBorder_gpu<uchar, 2>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream); template void copyMakeBorder_gpu<uchar, 2>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream);
template void copyMakeBorder_gpu<uchar, 3>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream); template void copyMakeBorder_gpu<uchar, 3>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream);
template void copyMakeBorder_gpu<uchar, 4>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream); template void copyMakeBorder_gpu<uchar, 4>(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderMode, const uchar* borderValue, cudaStream_t stream);
......
This diff is collapsed.
This diff is collapsed.
...@@ -329,11 +329,11 @@ void cv::gpu::copyMakeBorder(const GpuMat& src, GpuMat& dst, int top, int bottom ...@@ -329,11 +329,11 @@ void cv::gpu::copyMakeBorder(const GpuMat& src, GpuMat& dst, int top, int bottom
typedef void (*caller_t)(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderType, const Scalar& value, cudaStream_t stream); typedef void (*caller_t)(const PtrStepSzb& src, const PtrStepSzb& dst, int top, int left, int borderType, const Scalar& value, cudaStream_t stream);
static const caller_t callers[6][4] = static const caller_t callers[6][4] =
{ {
{ copyMakeBorder_caller<uchar, 1> , 0/*copyMakeBorder_caller<uchar, 2>*/ , copyMakeBorder_caller<uchar, 3> , copyMakeBorder_caller<uchar, 4>}, { copyMakeBorder_caller<uchar, 1> , copyMakeBorder_caller<uchar, 2> , copyMakeBorder_caller<uchar, 3> , copyMakeBorder_caller<uchar, 4>},
{0/*copyMakeBorder_caller<schar, 1>*/, 0/*copyMakeBorder_caller<schar, 2>*/ , 0/*copyMakeBorder_caller<schar, 3>*/, 0/*copyMakeBorder_caller<schar, 4>*/}, {0/*copyMakeBorder_caller<schar, 1>*/, 0/*copyMakeBorder_caller<schar, 2>*/ , 0/*copyMakeBorder_caller<schar, 3>*/, 0/*copyMakeBorder_caller<schar, 4>*/},
{ copyMakeBorder_caller<ushort, 1> , 0/*copyMakeBorder_caller<ushort, 2>*/, copyMakeBorder_caller<ushort, 3> , copyMakeBorder_caller<ushort, 4>}, { copyMakeBorder_caller<ushort, 1> , 0/*copyMakeBorder_caller<ushort, 2>*/, copyMakeBorder_caller<ushort, 3> , copyMakeBorder_caller<ushort, 4>},
{ copyMakeBorder_caller<short, 1> , 0/*copyMakeBorder_caller<short, 2>*/ , copyMakeBorder_caller<short, 3> , copyMakeBorder_caller<short, 4>}, { copyMakeBorder_caller<short, 1> , 0/*copyMakeBorder_caller<short, 2>*/ , copyMakeBorder_caller<short, 3> , copyMakeBorder_caller<short, 4>},
{0/*copyMakeBorder_caller<int, 1>*/ , 0/*copyMakeBorder_caller<int, 2>*/ , 0/*copyMakeBorder_caller<int, 3>*/ , 0/*copyMakeBorder_caller<int, 4>*/}, {0/*copyMakeBorder_caller<int, 1>*/, 0/*copyMakeBorder_caller<int, 2>*/ , 0/*copyMakeBorder_caller<int, 3>*/, 0/*copyMakeBorder_caller<int , 4>*/},
{ copyMakeBorder_caller<float, 1> , 0/*copyMakeBorder_caller<float, 2>*/ , copyMakeBorder_caller<float, 3> , copyMakeBorder_caller<float ,4>} { copyMakeBorder_caller<float, 1> , 0/*copyMakeBorder_caller<float, 2>*/ , copyMakeBorder_caller<float, 3> , copyMakeBorder_caller<float ,4>}
}; };
......
...@@ -72,8 +72,6 @@ PARAM_TEST_CASE(BilateralFilter, cv::gpu::DeviceInfo, cv::Size, MatType) ...@@ -72,8 +72,6 @@ PARAM_TEST_CASE(BilateralFilter, cv::gpu::DeviceInfo, cv::Size, MatType)
TEST_P(BilateralFilter, Accuracy) TEST_P(BilateralFilter, Accuracy)
{ {
cv::Mat src = randomMat(size, type); cv::Mat src = randomMat(size, type);
//cv::Mat src = readImage("hog/road.png", cv::IMREAD_GRAYSCALE);
//cv::Mat src = readImage("csstereobp/aloe-R.png", cv::IMREAD_GRAYSCALE);
src.convertTo(src, type); src.convertTo(src, type);
cv::gpu::GpuMat dst; cv::gpu::GpuMat dst;
...@@ -118,16 +116,16 @@ TEST_P(BruteForceNonLocalMeans, Regression) ...@@ -118,16 +116,16 @@ TEST_P(BruteForceNonLocalMeans, Regression)
cv::cvtColor(bgr, gray, CV_BGR2GRAY); cv::cvtColor(bgr, gray, CV_BGR2GRAY);
GpuMat dbgr, dgray; GpuMat dbgr, dgray;
cv::gpu::nonLocalMeans(GpuMat(bgr), dbgr, 10); cv::gpu::nonLocalMeans(GpuMat(bgr), dbgr, 20);
cv::gpu::nonLocalMeans(GpuMat(gray), dgray, 10); cv::gpu::nonLocalMeans(GpuMat(gray), dgray, 20);
#if 0 #if 0
dumpImage("denoising/denoised_lena_bgr.png", cv::Mat(dbgr)); dumpImage("denoising/nlm_denoised_lena_bgr.png", cv::Mat(dbgr));
dumpImage("denoising/denoised_lena_gray.png", cv::Mat(dgray)); dumpImage("denoising/nlm_denoised_lena_gray.png", cv::Mat(dgray));
#endif #endif
cv::Mat bgr_gold = readImage("denoising/denoised_lena_bgr.png", cv::IMREAD_COLOR); cv::Mat bgr_gold = readImage("denoising/nlm_denoised_lena_bgr.png", cv::IMREAD_COLOR);
cv::Mat gray_gold = readImage("denoising/denoised_lena_gray.png", cv::IMREAD_GRAYSCALE); cv::Mat gray_gold = readImage("denoising/nlm_denoised_lena_gray.png", cv::IMREAD_GRAYSCALE);
ASSERT_FALSE(bgr_gold.empty() || gray_gold.empty()); ASSERT_FALSE(bgr_gold.empty() || gray_gold.empty());
EXPECT_MAT_NEAR(bgr_gold, dbgr, 1e-4); EXPECT_MAT_NEAR(bgr_gold, dbgr, 1e-4);
...@@ -163,20 +161,22 @@ TEST_P(FastNonLocalMeans, Regression) ...@@ -163,20 +161,22 @@ TEST_P(FastNonLocalMeans, Regression)
cv::cvtColor(bgr, gray, CV_BGR2GRAY); cv::cvtColor(bgr, gray, CV_BGR2GRAY);
GpuMat dbgr, dgray; GpuMat dbgr, dgray;
cv::gpu::fastNlMeansDenoising(GpuMat(gray), dgray, 10); cv::gpu::FastNonLocalMeansDenoising fnlmd;
fnlmd.simpleMethod(GpuMat(gray), dgray, 20);
fnlmd.labMethod(GpuMat(bgr), dbgr, 20, 10);
#if 0 #if 0
//dumpImage("denoising/fnlm_denoised_lena_bgr.png", cv::Mat(dbgr)); //dumpImage("denoising/fnlm_denoised_lena_bgr.png", cv::Mat(dbgr));
dumpImage("denoising/fnlm_denoised_lena_gray.png", cv::Mat(dgray)); //dumpImage("denoising/fnlm_denoised_lena_gray.png", cv::Mat(dgray));
#endif #endif
//cv::Mat bgr_gold = readImage("denoising/denoised_lena_bgr.png", cv::IMREAD_COLOR); cv::Mat bgr_gold = readImage("denoising/fnlm_denoised_lena_bgr.png", cv::IMREAD_COLOR);
cv::Mat gray_gold = readImage("denoising/fnlm_denoised_lena_gray.png", cv::IMREAD_GRAYSCALE); cv::Mat gray_gold = readImage("denoising/fnlm_denoised_lena_gray.png", cv::IMREAD_GRAYSCALE);
ASSERT_FALSE(/*bgr_gold.empty() || */gray_gold.empty()); ASSERT_FALSE(bgr_gold.empty() || gray_gold.empty());
//EXPECT_MAT_NEAR(bgr_gold, dbgr, 1e-4);
EXPECT_MAT_NEAR(gray_gold, dgray, 1e-4);
EXPECT_MAT_NEAR(bgr_gold, dbgr, 1);
EXPECT_MAT_NEAR(gray_gold, dgray, 1);
} }
INSTANTIATE_TEST_CASE_P(GPU_Denoising, FastNonLocalMeans, ALL_DEVICES); INSTANTIATE_TEST_CASE_P(GPU_Denoising, FastNonLocalMeans, ALL_DEVICES);
......
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