Commit cec59eb6 authored by Clément Bœsch's avatar Clément Bœsch

avfilter/dctdnoiz: add 8x8 dct and make it the default

8x8 is about 5x faster than 16x16 on 1080p input. Since a block size of
8x8 makes the filter almost usable (time wise) and it's not obvious if
8x8 or 16x16 is better from a quality PoV (it really depends on the
input and parameters), the filter now defaults to 8x8, and as a result
libavfilter is micro bumped.
parent ec0b08d2
...@@ -3219,7 +3219,7 @@ curves=psfile='MyCurvesPresets/purple.asv':green='0.45/0.53' ...@@ -3219,7 +3219,7 @@ curves=psfile='MyCurvesPresets/purple.asv':green='0.45/0.53'
Denoise frames using 2D DCT (frequency domain filtering). Denoise frames using 2D DCT (frequency domain filtering).
This filter is not designed for real time and can be extremely slow. This filter is not designed for real time.
The filter accepts the following options: The filter accepts the following options:
...@@ -3235,14 +3235,14 @@ If you need a more advanced filtering, see @option{expr}. ...@@ -3235,14 +3235,14 @@ If you need a more advanced filtering, see @option{expr}.
Default is @code{0}. Default is @code{0}.
@item overlap @item overlap
Set number overlapping pixels for each block. Each block is of size Set number overlapping pixels for each block. Since the filter can be slow, you
@code{16x16}. Since the filter can be slow, you may want to reduce this value, may want to reduce this value, at the cost of a less effective filter and the
at the cost of a less effective filter and the risk of various artefacts. risk of various artefacts.
If the overlapping value doesn't allow to process the whole input width or If the overlapping value doesn't allow to process the whole input width or
height, a warning will be displayed and according borders won't be denoised. height, a warning will be displayed and according borders won't be denoised.
Default value is @code{15}. Default value is @var{blocksize}-1, which is the best possible setting.
@item expr, e @item expr, e
Set the coefficient factor expression. Set the coefficient factor expression.
...@@ -3254,6 +3254,15 @@ If this is option is set, the @option{sigma} option will be ignored. ...@@ -3254,6 +3254,15 @@ If this is option is set, the @option{sigma} option will be ignored.
The absolute value of the coefficient can be accessed through the @var{c} The absolute value of the coefficient can be accessed through the @var{c}
variable. variable.
@item n
Set the @var{blocksize} using the number of bits. @code{1<<@var{n}} defines the
@var{blocksize}, which is the width and height of the processed blocks.
The default value is @var{3} (8x8) and can be raised to @var{4} for a
@var{blocksize} of 16x16. Note that changing this setting has huge consequences
on the speed processing. Also, a larger block size does not necessarily means a
better de-noising.
@end table @end table
@subsection Examples @subsection Examples
...@@ -3268,6 +3277,11 @@ The same operation can be achieved using the expression system: ...@@ -3268,6 +3277,11 @@ The same operation can be achieved using the expression system:
dctdnoiz=e='gte(c, 4.5*3)' dctdnoiz=e='gte(c, 4.5*3)'
@end example @end example
Violent denoise using a block size of @code{16x16}:
@example
dctdnoiz=15:n=4
@end example
@anchor{decimate} @anchor{decimate}
@section decimate @section decimate
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
#define LIBAVFILTER_VERSION_MAJOR 4 #define LIBAVFILTER_VERSION_MAJOR 4
#define LIBAVFILTER_VERSION_MINOR 11 #define LIBAVFILTER_VERSION_MINOR 11
#define LIBAVFILTER_VERSION_MICRO 102 #define LIBAVFILTER_VERSION_MICRO 103
#define LIBAVFILTER_VERSION_INT AV_VERSION_INT(LIBAVFILTER_VERSION_MAJOR, \ #define LIBAVFILTER_VERSION_INT AV_VERSION_INT(LIBAVFILTER_VERSION_MAJOR, \
LIBAVFILTER_VERSION_MINOR, \ LIBAVFILTER_VERSION_MINOR, \
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
*/ */
/** /**
* A simple, relatively efficient and extremely slow DCT image denoiser. * A simple, relatively efficient and slow DCT image denoiser.
* *
* @see http://www.ipol.im/pub/art/2011/ys-dct/ * @see http://www.ipol.im/pub/art/2011/ys-dct/
* *
...@@ -28,14 +28,12 @@ ...@@ -28,14 +28,12 @@
* Tasche (DOI: 10.1016/j.laa.2004.07.015). * Tasche (DOI: 10.1016/j.laa.2004.07.015).
*/ */
#include "libavutil/avassert.h"
#include "libavutil/eval.h" #include "libavutil/eval.h"
#include "libavutil/opt.h" #include "libavutil/opt.h"
#include "drawutils.h" #include "drawutils.h"
#include "internal.h" #include "internal.h"
#define NBITS 4
#define BSIZE (1<<(NBITS))
static const char *const var_names[] = { "c", NULL }; static const char *const var_names[] = { "c", NULL };
enum { VAR_C, VAR_VARS_NB }; enum { VAR_C, VAR_VARS_NB };
...@@ -55,32 +53,122 @@ typedef struct DCTdnoizContext { ...@@ -55,32 +53,122 @@ typedef struct DCTdnoizContext {
float *weights; // dct coeff are cumulated with overlapping; these values are used for averaging float *weights; // dct coeff are cumulated with overlapping; these values are used for averaging
int p_linesize; // line sizes for color and weights int p_linesize; // line sizes for color and weights
int overlap; // number of block overlapping pixels int overlap; // number of block overlapping pixels
int step; // block step increment (BSIZE - overlap) int step; // block step increment (blocksize - overlap)
int n; // 1<<n is the block size
int bsize; // block size, 1<<n
void (*filter_freq_func)(struct DCTdnoizContext *s, void (*filter_freq_func)(struct DCTdnoizContext *s,
const float *src, int src_linesize, const float *src, int src_linesize,
float *dst, int dst_linesize); float *dst, int dst_linesize);
} DCTdnoizContext; } DCTdnoizContext;
#define MIN_NBITS 3 /* blocksize = 1<<3 = 8 */
#define MAX_NBITS 4 /* blocksize = 1<<4 = 16 */
#define DEFAULT_NBITS 3
#define OFFSET(x) offsetof(DCTdnoizContext, x) #define OFFSET(x) offsetof(DCTdnoizContext, x)
#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
static const AVOption dctdnoiz_options[] = { static const AVOption dctdnoiz_options[] = {
{ "sigma", "set noise sigma constant", OFFSET(sigma), AV_OPT_TYPE_FLOAT, {.dbl=0}, 0, 999, .flags = FLAGS }, { "sigma", "set noise sigma constant", OFFSET(sigma), AV_OPT_TYPE_FLOAT, {.dbl=0}, 0, 999, .flags = FLAGS },
{ "s", "set noise sigma constant", OFFSET(sigma), AV_OPT_TYPE_FLOAT, {.dbl=0}, 0, 999, .flags = FLAGS }, { "s", "set noise sigma constant", OFFSET(sigma), AV_OPT_TYPE_FLOAT, {.dbl=0}, 0, 999, .flags = FLAGS },
{ "overlap", "set number of block overlapping pixels", OFFSET(overlap), AV_OPT_TYPE_INT, {.i64=(1<<NBITS)-1}, 0, (1<<NBITS)-1, .flags = FLAGS }, { "overlap", "set number of block overlapping pixels", OFFSET(overlap), AV_OPT_TYPE_INT, {.i64=-1}, -1, (1<<MAX_NBITS)-1, .flags = FLAGS },
{ "expr", "set coefficient factor expression", OFFSET(expr_str), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS }, { "expr", "set coefficient factor expression", OFFSET(expr_str), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS },
{ "e", "set coefficient factor expression", OFFSET(expr_str), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS }, { "e", "set coefficient factor expression", OFFSET(expr_str), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS },
{ "n", "set the block size, expressed in bits", OFFSET(n), AV_OPT_TYPE_INT, {.i64=DEFAULT_NBITS}, MIN_NBITS, MAX_NBITS, .flags = FLAGS },
{ NULL } { NULL }
}; };
AVFILTER_DEFINE_CLASS(dctdnoiz); AVFILTER_DEFINE_CLASS(dctdnoiz);
static void av_always_inline fdct8_1d(float *dst, const float *src,
int dst_stridea, int dst_strideb,
int src_stridea, int src_strideb)
{
int i;
for (i = 0; i < 8; i++) {
const float x00 = src[0*src_stridea] + src[7*src_stridea];
const float x01 = src[1*src_stridea] + src[6*src_stridea];
const float x02 = src[2*src_stridea] + src[5*src_stridea];
const float x03 = src[3*src_stridea] + src[4*src_stridea];
const float x04 = src[0*src_stridea] - src[7*src_stridea];
const float x05 = src[1*src_stridea] - src[6*src_stridea];
const float x06 = src[2*src_stridea] - src[5*src_stridea];
const float x07 = src[3*src_stridea] - src[4*src_stridea];
const float x08 = x00 + x03;
const float x09 = x01 + x02;
const float x0a = x00 - x03;
const float x0b = x01 - x02;
const float x0c = 1.38703984532215*x04 + 0.275899379282943*x07;
const float x0d = 1.17587560241936*x05 + 0.785694958387102*x06;
const float x0e = -0.785694958387102*x05 + 1.17587560241936*x06;
const float x0f = 0.275899379282943*x04 - 1.38703984532215*x07;
const float x10 = 0.353553390593274 * (x0c - x0d);
const float x11 = 0.353553390593274 * (x0e - x0f);
dst[0*dst_stridea] = 0.353553390593274 * (x08 + x09);
dst[1*dst_stridea] = 0.353553390593274 * (x0c + x0d);
dst[2*dst_stridea] = 0.461939766255643*x0a + 0.191341716182545*x0b;
dst[3*dst_stridea] = 0.707106781186547 * (x10 - x11);
dst[4*dst_stridea] = 0.353553390593274 * (x08 - x09);
dst[5*dst_stridea] = 0.707106781186547 * (x10 + x11);
dst[6*dst_stridea] = 0.191341716182545*x0a - 0.461939766255643*x0b;
dst[7*dst_stridea] = 0.353553390593274 * (x0e + x0f);
dst += dst_strideb;
src += src_strideb;
}
}
static void av_always_inline idct8_1d(float *dst, const float *src,
int dst_stridea, int dst_strideb,
int src_stridea, int src_strideb,
int add)
{
int i;
for (i = 0; i < 8; i++) {
const float x00 = 1.4142135623731*src[0*src_stridea];
const float x01 = 1.38703984532215*src[1*src_stridea] + 0.275899379282943*src[7*src_stridea];
const float x02 = 1.30656296487638*src[2*src_stridea] + 0.541196100146197*src[6*src_stridea];
const float x03 = 1.17587560241936*src[3*src_stridea] + 0.785694958387102*src[5*src_stridea];
const float x04 = 1.4142135623731*src[4*src_stridea];
const float x05 = -0.785694958387102*src[3*src_stridea] + 1.17587560241936*src[5*src_stridea];
const float x06 = 0.541196100146197*src[2*src_stridea] - 1.30656296487638*src[6*src_stridea];
const float x07 = -0.275899379282943*src[1*src_stridea] + 1.38703984532215*src[7*src_stridea];
const float x09 = x00 + x04;
const float x0a = x01 + x03;
const float x0b = 1.4142135623731*x02;
const float x0c = x00 - x04;
const float x0d = x01 - x03;
const float x0e = 0.353553390593274 * (x09 - x0b);
const float x0f = 0.353553390593274 * (x0c + x0d);
const float x10 = 0.353553390593274 * (x0c - x0d);
const float x11 = 1.4142135623731*x06;
const float x12 = x05 + x07;
const float x13 = x05 - x07;
const float x14 = 0.353553390593274 * (x11 + x12);
const float x15 = 0.353553390593274 * (x11 - x12);
const float x16 = 0.5*x13;
const float x08 = -x15;
dst[0*dst_stridea] = (add ? dst[ 0*dst_stridea] : 0) + 0.25 * (x09 + x0b) + 0.353553390593274*x0a;
dst[1*dst_stridea] = (add ? dst[ 1*dst_stridea] : 0) + 0.707106781186547 * (x0f - x08);
dst[2*dst_stridea] = (add ? dst[ 2*dst_stridea] : 0) + 0.707106781186547 * (x0f + x08);
dst[3*dst_stridea] = (add ? dst[ 3*dst_stridea] : 0) + 0.707106781186547 * (x0e + x16);
dst[4*dst_stridea] = (add ? dst[ 4*dst_stridea] : 0) + 0.707106781186547 * (x0e - x16);
dst[5*dst_stridea] = (add ? dst[ 5*dst_stridea] : 0) + 0.707106781186547 * (x10 - x14);
dst[6*dst_stridea] = (add ? dst[ 6*dst_stridea] : 0) + 0.707106781186547 * (x10 + x14);
dst[7*dst_stridea] = (add ? dst[ 7*dst_stridea] : 0) + 0.25 * (x09 + x0b) - 0.353553390593274*x0a;
dst += dst_strideb;
src += src_strideb;
}
}
static void av_always_inline fdct16_1d(float *dst, const float *src, static void av_always_inline fdct16_1d(float *dst, const float *src,
int dst_stridea, int dst_strideb, int dst_stridea, int dst_strideb,
int src_stridea, int src_strideb) int src_stridea, int src_strideb)
{ {
int i; int i;
for (i = 0; i < BSIZE; i++) { for (i = 0; i < 16; i++) {
const float x00 = src[ 0*src_stridea] + src[15*src_stridea]; const float x00 = src[ 0*src_stridea] + src[15*src_stridea];
const float x01 = src[ 1*src_stridea] + src[14*src_stridea]; const float x01 = src[ 1*src_stridea] + src[14*src_stridea];
const float x02 = src[ 2*src_stridea] + src[13*src_stridea]; const float x02 = src[ 2*src_stridea] + src[13*src_stridea];
...@@ -165,7 +253,7 @@ static void av_always_inline idct16_1d(float *dst, const float *src, ...@@ -165,7 +253,7 @@ static void av_always_inline idct16_1d(float *dst, const float *src,
{ {
int i; int i;
for (i = 0; i < BSIZE; i++) { for (i = 0; i < 16; i++) {
const float x00 = 1.4142135623731 *src[ 0*src_stridea]; const float x00 = 1.4142135623731 *src[ 0*src_stridea];
const float x01 = 1.40740373752638 *src[ 1*src_stridea] + 0.138617169199091*src[15*src_stridea]; const float x01 = 1.40740373752638 *src[ 1*src_stridea] + 0.138617169199091*src[15*src_stridea];
const float x02 = 1.38703984532215 *src[ 2*src_stridea] + 0.275899379282943*src[14*src_stridea]; const float x02 = 1.38703984532215 *src[ 2*src_stridea] + 0.275899379282943*src[14*src_stridea];
...@@ -256,55 +344,60 @@ static void av_always_inline idct16_1d(float *dst, const float *src, ...@@ -256,55 +344,60 @@ static void av_always_inline idct16_1d(float *dst, const float *src,
} }
} }
static av_always_inline void filter_freq(const float *src, int src_linesize, #define DEF_FILTER_FREQ_FUNCS(bsize) \
float *dst, int dst_linesize, static av_always_inline void filter_freq_##bsize(const float *src, int src_linesize, \
AVExpr *expr, double *var_values, float *dst, int dst_linesize, \
int sigma_th) AVExpr *expr, double *var_values, \
{ int sigma_th) \
unsigned i; { \
DECLARE_ALIGNED(32, float, tmp_block1)[BSIZE * BSIZE]; unsigned i; \
DECLARE_ALIGNED(32, float, tmp_block2)[BSIZE * BSIZE]; DECLARE_ALIGNED(32, float, tmp_block1)[bsize * bsize]; \
DECLARE_ALIGNED(32, float, tmp_block2)[bsize * bsize]; \
/* forward DCT */ \
fdct16_1d(tmp_block1, src, 1, BSIZE, 1, src_linesize); /* forward DCT */ \
fdct16_1d(tmp_block2, tmp_block1, BSIZE, 1, BSIZE, 1); fdct##bsize##_1d(tmp_block1, src, 1, bsize, 1, src_linesize); \
fdct##bsize##_1d(tmp_block2, tmp_block1, bsize, 1, bsize, 1); \
for (i = 0; i < BSIZE*BSIZE; i++) { \
float *b = &tmp_block2[i]; for (i = 0; i < bsize*bsize; i++) { \
/* frequency filtering */ float *b = &tmp_block2[i]; \
if (expr) { /* frequency filtering */ \
var_values[VAR_C] = FFABS(*b); if (expr) { \
*b *= av_expr_eval(expr, var_values, NULL); var_values[VAR_C] = FFABS(*b); \
} else { *b *= av_expr_eval(expr, var_values, NULL); \
if (FFABS(*b) < sigma_th) } else { \
*b = 0; if (FFABS(*b) < sigma_th) \
} *b = 0; \
} } \
} \
/* inverse DCT */ \
idct16_1d(tmp_block1, tmp_block2, 1, BSIZE, 1, BSIZE, 0); /* inverse DCT */ \
idct16_1d(dst, tmp_block1, dst_linesize, 1, BSIZE, 1, 1); idct##bsize##_1d(tmp_block1, tmp_block2, 1, bsize, 1, bsize, 0); \
idct##bsize##_1d(dst, tmp_block1, dst_linesize, 1, bsize, 1, 1); \
} \
\
static void filter_freq_sigma_##bsize(DCTdnoizContext *s, \
const float *src, int src_linesize, \
float *dst, int dst_linesize) \
{ \
filter_freq_##bsize(src, src_linesize, dst, dst_linesize, NULL, NULL, s->th); \
} \
\
static void filter_freq_expr_##bsize(DCTdnoizContext *s, \
const float *src, int src_linesize, \
float *dst, int dst_linesize) \
{ \
filter_freq_##bsize(src, src_linesize, dst, dst_linesize, s->expr, s->var_values, 0); \
} }
static void filter_freq_sigma(DCTdnoizContext *s, DEF_FILTER_FREQ_FUNCS(8)
const float *src, int src_linesize, DEF_FILTER_FREQ_FUNCS(16)
float *dst, int dst_linesize)
{
filter_freq(src, src_linesize, dst, dst_linesize, NULL, NULL, s->th);
}
static void filter_freq_expr(DCTdnoizContext *s,
const float *src, int src_linesize,
float *dst, int dst_linesize)
{
filter_freq(src, src_linesize, dst, dst_linesize, s->expr, s->var_values, 0);
}
static int config_input(AVFilterLink *inlink) static int config_input(AVFilterLink *inlink)
{ {
AVFilterContext *ctx = inlink->dst; AVFilterContext *ctx = inlink->dst;
DCTdnoizContext *s = ctx->priv; DCTdnoizContext *s = ctx->priv;
int i, x, y, bx, by, linesize, *iweights; int i, x, y, bx, by, linesize, *iweights;
const int bsize = 1 << s->n;
const float dct_3x3[3][3] = { const float dct_3x3[3][3] = {
{ 1./sqrt(3), 1./sqrt(3), 1./sqrt(3) }, { 1./sqrt(3), 1./sqrt(3), 1./sqrt(3) },
{ 1./sqrt(2), 0, -1./sqrt(2) }, { 1./sqrt(2), 0, -1./sqrt(2) },
...@@ -317,8 +410,8 @@ static int config_input(AVFilterLink *inlink) ...@@ -317,8 +410,8 @@ static int config_input(AVFilterLink *inlink)
for (x = 0; x < 3; x++) for (x = 0; x < 3; x++)
s->color_dct[y][x] = dct_3x3[rgba_map[y]][rgba_map[x]]; s->color_dct[y][x] = dct_3x3[rgba_map[y]][rgba_map[x]];
s->pr_width = inlink->w - (inlink->w - BSIZE) % s->step; s->pr_width = inlink->w - (inlink->w - bsize) % s->step;
s->pr_height = inlink->h - (inlink->h - BSIZE) % s->step; s->pr_height = inlink->h - (inlink->h - bsize) % s->step;
if (s->pr_width != inlink->w) if (s->pr_width != inlink->w)
av_log(ctx, AV_LOG_WARNING, "The last %d horizontal pixels won't be denoised\n", av_log(ctx, AV_LOG_WARNING, "The last %d horizontal pixels won't be denoised\n",
inlink->w - s->pr_width); inlink->w - s->pr_width);
...@@ -341,10 +434,10 @@ static int config_input(AVFilterLink *inlink) ...@@ -341,10 +434,10 @@ static int config_input(AVFilterLink *inlink)
iweights = av_calloc(s->pr_height, linesize * sizeof(*iweights)); iweights = av_calloc(s->pr_height, linesize * sizeof(*iweights));
if (!iweights) if (!iweights)
return AVERROR(ENOMEM); return AVERROR(ENOMEM);
for (y = 0; y < s->pr_height - BSIZE + 1; y += s->step) for (y = 0; y < s->pr_height - bsize + 1; y += s->step)
for (x = 0; x < s->pr_width - BSIZE + 1; x += s->step) for (x = 0; x < s->pr_width - bsize + 1; x += s->step)
for (by = 0; by < BSIZE; by++) for (by = 0; by < bsize; by++)
for (bx = 0; bx < BSIZE; bx++) for (bx = 0; bx < bsize; bx++)
iweights[(y + by)*linesize + x + bx]++; iweights[(y + by)*linesize + x + bx]++;
for (y = 0; y < s->pr_height; y++) for (y = 0; y < s->pr_height; y++)
for (x = 0; x < s->pr_width; x++) for (x = 0; x < s->pr_width; x++)
...@@ -358,18 +451,37 @@ static av_cold int init(AVFilterContext *ctx) ...@@ -358,18 +451,37 @@ static av_cold int init(AVFilterContext *ctx)
{ {
DCTdnoizContext *s = ctx->priv; DCTdnoizContext *s = ctx->priv;
s->bsize = 1 << s->n;
if (s->overlap == -1)
s->overlap = s->bsize - 1;
if (s->overlap > s->bsize - 1) {
av_log(s, AV_LOG_ERROR, "Overlap value can not except %d "
"with a block size of %dx%d\n",
s->bsize - 1, s->bsize, s->bsize);
return AVERROR(EINVAL);
}
if (s->expr_str) { if (s->expr_str) {
int ret = av_expr_parse(&s->expr, s->expr_str, var_names, int ret = av_expr_parse(&s->expr, s->expr_str, var_names,
NULL, NULL, NULL, NULL, 0, ctx); NULL, NULL, NULL, NULL, 0, ctx);
if (ret < 0) if (ret < 0)
return ret; return ret;
s->filter_freq_func = filter_freq_expr; switch (s->n) {
case 3: s->filter_freq_func = filter_freq_expr_8; break;
case 4: s->filter_freq_func = filter_freq_expr_16; break;
default: av_assert0(0);
}
} else { } else {
s->filter_freq_func = filter_freq_sigma; switch (s->n) {
case 3: s->filter_freq_func = filter_freq_sigma_8; break;
case 4: s->filter_freq_func = filter_freq_sigma_16; break;
default: av_assert0(0);
}
} }
s->th = s->sigma * 3.; s->th = s->sigma * 3.;
s->step = BSIZE - s->overlap; s->step = s->bsize - s->overlap;
return 0; return 0;
} }
...@@ -445,8 +557,8 @@ static void filter_plane(AVFilterContext *ctx, ...@@ -445,8 +557,8 @@ static void filter_plane(AVFilterContext *ctx,
memset(dst, 0, h * dst_linesize * sizeof(*dst)); memset(dst, 0, h * dst_linesize * sizeof(*dst));
// block dct sums // block dct sums
for (y = 0; y < h - BSIZE + 1; y += s->step) { for (y = 0; y < h - s->bsize + 1; y += s->step) {
for (x = 0; x < w - BSIZE + 1; x += s->step) for (x = 0; x < w - s->bsize + 1; x += s->step)
s->filter_freq_func(s, src + x, src_linesize, s->filter_freq_func(s, src + x, src_linesize,
dst + x, dst_linesize); dst + x, dst_linesize);
src += s->step * src_linesize; src += s->step * src_linesize;
......
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