mirror of
https://github.com/ultimatepp/ultimatepp.git
synced 2026-07-11 22:03:01 -06:00
.uppdev (image filter)
git-svn-id: svn://ultimatepp.org/upp/trunk@5982 f0d560ea-af0d-0410-9eb7-867de7ffcac7
This commit is contained in:
parent
d3583ad02f
commit
f968ccdf77
6 changed files with 359 additions and 17 deletions
|
|
@ -10,9 +10,11 @@ file
|
|||
bicubic.h,
|
||||
exact.cpp,
|
||||
fast.cpp,
|
||||
fast2.cpp,
|
||||
DownScale.cpp,
|
||||
image.iml,
|
||||
main.cpp;
|
||||
main.cpp,
|
||||
help.txt;
|
||||
|
||||
mainconfig
|
||||
"" = "GUI SSE2";
|
||||
|
|
|
|||
|
|
@ -12,4 +12,6 @@ enum {
|
|||
Image RescaleWithKernelE(const Image& _img, int cx, int cy, double (*kernel)(double x), int a, int method = DOWNSCALE_WIDE);
|
||||
Image RescaleWithKernel(const Image& _img, int cx, int cy, double (*kernel)(double x), int a, int method = DOWNSCALE_WIDE);
|
||||
|
||||
Image RescaleWithKernel2(const Image& _img, int cx, int cy, double (*kernel)(double x), int a, int method = DOWNSCALE_WIDE);
|
||||
|
||||
Image DownScale(const Image& img, int nx, int ny);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ static int sGeta(int a, int src, int tgt, int method)
|
|||
{
|
||||
if(method != DOWNSCALE_WIDE)
|
||||
return a;
|
||||
return max(src / tgt, 1) * a;
|
||||
return min(max(src / tgt, 1) * a, 7);
|
||||
}
|
||||
|
||||
force_inline
|
||||
|
|
|
|||
108
uppdev/BiCubic/fast2.cpp
Normal file
108
uppdev/BiCubic/fast2.cpp
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#include "bicubic.h"
|
||||
|
||||
#if 1
|
||||
|
||||
#define LDUMP(x) // DUMP(x)
|
||||
|
||||
static const int *sGetKernel(double (*kfn)(double x), int a, int shift)
|
||||
{
|
||||
INTERLOCKED {
|
||||
static VectorMap<Tuple3<uintptr_t, int, int>, Buffer<int> > kache;
|
||||
Tuple3<uintptr_t, int, int> key = MakeTuple((uintptr_t)kfn, a, shift);
|
||||
Buffer<int> *k = kache.FindPtr(key);
|
||||
if(k)
|
||||
return *k;
|
||||
RTIMING("GenKernel");
|
||||
Buffer<int>& ktab = kache.GetAdd(key);
|
||||
ktab.Alloc(((2 * a) << shift) + 1);
|
||||
for(int i = 0; i < ((2 * a) << shift) + 1; i++)
|
||||
ktab[i] = int((1 << shift) * (*kfn)((double)i / (1 << shift) - a));
|
||||
return ktab;
|
||||
}
|
||||
}
|
||||
|
||||
force_inline
|
||||
int sGetk(const int *kernel, int x, int a, int shift)
|
||||
{
|
||||
x += a << shift;
|
||||
ASSERT(x >= 0 && x < ((2 * a) << shift) + 1);
|
||||
return kernel[x];
|
||||
}
|
||||
|
||||
static int sGeta(int a, int src, int tgt, int method, int& shift)
|
||||
{
|
||||
if(method != DOWNSCALE_WIDE)
|
||||
return a;
|
||||
int n = min(max(src / tgt, 1) * a, 31);
|
||||
shift = 8 - n / 8;
|
||||
return n;
|
||||
}
|
||||
|
||||
Image RescaleWithKernel2(const Image& img, int cx, int cy, double (*kfn)(double x), int a, int method)
|
||||
{
|
||||
Size isz = img.GetSize();
|
||||
|
||||
int shiftx, shifty;
|
||||
int ax = sGeta(a, isz.cx, cx, method, shiftx);
|
||||
int ay = sGeta(a, isz.cy, cy, method, shifty);
|
||||
|
||||
int shift = min(shiftx, shifty);
|
||||
|
||||
const int *kernel = sGetKernel(kfn, a, shift);
|
||||
|
||||
Buffer<int> px(cx * 2 * ax * 2 * ay * 2);
|
||||
int *xd = px;
|
||||
for(int x = 0; x < cx; x++) {
|
||||
int sx = x * isz.cx / cx;
|
||||
int dx = ((x * isz.cx) << shift) / cx - (sx << shift);
|
||||
for(int yy = -ay + 1; yy <= ay; yy++) {
|
||||
for(int xx = -ax + 1; xx <= ax; xx++) {
|
||||
*xd++ = minmax(sx + xx, 0, isz.cx - 1);
|
||||
*xd++ = sGetk(kernel, ((xx << shift) - dx) * a / ax, a, shift);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageBuffer ib(cx, cy);
|
||||
RGBA *t = ~ib;
|
||||
for(int y = 0; y < cy; y++) {
|
||||
int sy = y * isz.cy / cy;
|
||||
int dy = ((y * isz.cy) << shift) / cy - (sy << shift);
|
||||
xd = ~px;
|
||||
Buffer<int> py(2 * ay * 2);
|
||||
int *yd = py;
|
||||
for(int yy = -ay + 1; yy <= ay; yy++) {
|
||||
*yd++ = sGetk(kernel, ((yy << shift) - dy) * a / ay, a, shift);
|
||||
*yd++ = minmax(sy + yy, 0, isz.cy - 1);
|
||||
}
|
||||
for(int x = 0; x < cx; x++) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
int alpha = 0;
|
||||
int w = 0;
|
||||
yd = py;
|
||||
for(int yy = 2 * ay; yy-- > 0;) {
|
||||
int ky = *yd++;
|
||||
const RGBA *l = img[*yd++];
|
||||
for(int xx = 2 * ax; xx-- > 0;) {
|
||||
const RGBA& s = l[*xd++];
|
||||
int weight = ky * *xd++;
|
||||
red += weight * s.r;
|
||||
green += weight * s.g;
|
||||
blue += weight * s.b;
|
||||
alpha += weight * s.a;
|
||||
w += weight;
|
||||
}
|
||||
}
|
||||
t->r = Saturate255(red / w);
|
||||
t->g = Saturate255(green / w);
|
||||
t->b = Saturate255(blue / w);
|
||||
t->a = Saturate255(alpha / w);
|
||||
t++;
|
||||
}
|
||||
}
|
||||
return ib;
|
||||
}
|
||||
|
||||
#endif
|
||||
118
uppdev/BiCubic/help.txt
Normal file
118
uppdev/BiCubic/help.txt
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#include "bicubic.h"
|
||||
|
||||
#if 1
|
||||
|
||||
#define LDUMP(x) // DUMP(x)
|
||||
|
||||
#define up(x) ((x) << 8)
|
||||
#define dn(x) ((x + (1 << 7)) >> 8)
|
||||
#define dn2(x) ((x + (1 << 15)) >> 16)
|
||||
|
||||
static int sGeta(int a, int src, int tgt, int method)
|
||||
{
|
||||
if(method != DOWNSCALE_WIDE)
|
||||
return a;
|
||||
return min(max(src / tgt, 1) * a, 7);
|
||||
}
|
||||
|
||||
force_inline
|
||||
int sGetk(const int *kernel, int x, int a)
|
||||
{
|
||||
x += up(a);
|
||||
ASSERT(x >= 0 && x < up(2 * a) + 1);
|
||||
return kernel[x];
|
||||
}
|
||||
|
||||
static const int *sGetKernel(double (*kfn)(double x), int a)
|
||||
{
|
||||
INTERLOCKED {
|
||||
static VectorMap<Tuple2<uintptr_t, int>, Buffer<int> > kache;
|
||||
Tuple2<uintptr_t, int> key = MakeTuple((uintptr_t)kfn, a);
|
||||
Buffer<int> *k = kache.FindPtr(key);
|
||||
if(k)
|
||||
return *k;
|
||||
RTIMING("GenKernel");
|
||||
Buffer<int>& ktab = kache.GetAdd(key);
|
||||
ktab.Alloc(up(2 * a) + 1);
|
||||
for(int i = 0; i < up(2 * a) + 1; i++)
|
||||
ktab[i] = int(up(1) * (*kfn)((double)i / up(1) - a));
|
||||
return ktab;
|
||||
}
|
||||
}
|
||||
|
||||
Image RescaleWithKernel2(const Image& img, int cx, int cy, double (*kfn)(double x), int a, int method)
|
||||
{
|
||||
// ToDo: Cache this!
|
||||
const int *kernel = sGetKernel(kfn, a);
|
||||
|
||||
Size isz = img.GetSize();
|
||||
|
||||
int ax = sGeta(a, isz.cx, cx, method);
|
||||
int ay = sGeta(a, isz.cy, cy, method);
|
||||
|
||||
Buffer<int> px(cx * 2 * ax * 2 * ay * 2);
|
||||
int *xd = px;
|
||||
for(int x = 0; x < cx; x++) {
|
||||
int sx = x * isz.cx / cx;
|
||||
int dx = up(x * isz.cx) / cx - up(sx);
|
||||
for(int yy = -ay + 1; yy <= ay; yy++) {
|
||||
for(int xx = -ax + 1; xx <= ax; xx++) {
|
||||
*xd++ = minmax(sx + xx, 0, isz.cx - 1);
|
||||
*xd++ = sGetk(kernel, (up(xx) - dx) * a / ax, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageBuffer ib(cx, cy);
|
||||
RGBA *t = ~ib;
|
||||
for(int y = 0; y < cy; y++) {
|
||||
int sy = y * isz.cy / cy;
|
||||
int dy = up(y * isz.cy) / cy - up(sy);
|
||||
const int *xd = ~px;
|
||||
for(int x = 0; x < cx; x++) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
int alpha = 0;
|
||||
int w = 0;
|
||||
for(int yy = -ay + 1; yy <= ay; yy++) {
|
||||
int ky = sGetk(kernel, (up(yy) - dy) * a / ay, a);
|
||||
const RGBA *l = img[minmax(sy + yy, 0, isz.cy - 1)];
|
||||
for(int xx = -ax + 1; xx <= ax; xx++) {
|
||||
const RGBA& s = l[*xd++];
|
||||
int weight = ky * *xd++;
|
||||
LDUMP(weight);
|
||||
red += weight * s.r;
|
||||
green += weight * s.g;
|
||||
blue += weight * s.b;
|
||||
LDUMP((int)s.a);
|
||||
alpha += weight * s.a;
|
||||
LDUMP(alpha);
|
||||
w += weight;
|
||||
}
|
||||
}
|
||||
t->r = Saturate255(red / w);
|
||||
t->g = Saturate255(green / w);
|
||||
t->b = Saturate255(blue / w);
|
||||
t->a = Saturate255(alpha / w);
|
||||
LDUMP((int)t->a);
|
||||
LDUMP(w);
|
||||
t++;
|
||||
}
|
||||
}
|
||||
return ib;
|
||||
}
|
||||
|
||||
#endif
|
||||
----------------
|
||||
TIMING Rescale Linear integer optimized: 79.00 ms - 789.97 us (79.00 ms / 100 ), min: 0.00 ns, max: 1.00 ms, nesting: 1 - 100
|
||||
TIMING GenKernel : 0.00 ns - 0.00 ns ( 0.00 ns / 1 ), min: 0.00 ns, max: 0.00 ns, nesting: 1 - 1
|
||||
TIMING Rescale Linear integer: 139.00 ms - 1.39 ms (139.00 ms / 100 ), min: 1.00 ms, max: 2.00 ms, nesting: 1 - 100
|
||||
TIMING Rescale : 20.00 ms - 199.97 us (20.00 ms / 100 ), min: 0.00 ns, max: 1.00 ms, nesting: 1 - 100
|
||||
TIMING GenKernel : 0.00 ns - 0.00 ns ( 0.00 ns / 2 ), min: 0.00 ns, max: 0.00 ns, nesting: 1 - 2
|
||||
----------------
|
||||
TIMING Rescale Linear integer optimized: 79.00 ms - 789.97 us (79.00 ms / 100 ), min: 0.00 ns, max: 1.00 ms, nesting: 1 - 100
|
||||
TIMING GenKernel : 0.00 ns - 0.00 ns ( 0.00 ns / 1 ), min: 0.00 ns, max: 0.00 ns, nesting: 1 - 1
|
||||
TIMING Rescale Linear integer: 140.00 ms - 1.40 ms (140.00 ms / 100 ), min: 1.00 ms, max: 2.00 ms, nesting: 1 - 100
|
||||
TIMING Rescale : 19.00 ms - 189.97 us (19.00 ms / 100 ), min: 0.00 ns, max: 1.00 ms, nesting: 1 - 100
|
||||
TIMING GenKernel : 0.00 ns - 0.00 ns ( 0.00 ns / 2 ), min: 0.00 ns, max: 0.00 ns, nesting: 1 - 2
|
||||
|
|
@ -28,6 +28,65 @@ double BiCubicKernel2(double x)
|
|||
return (1 / 6.0) * r;
|
||||
}
|
||||
|
||||
double BiCubic(double x, double a)
|
||||
{
|
||||
x = fabs(x);
|
||||
return x <= 1 ? (a + 2) * x * x * x - (a + 3) * x * x + 1 :
|
||||
x <= 2 ? a * x * x * x - 5 * a * x * x + 8 * a * x - 4 * a :
|
||||
0;
|
||||
}
|
||||
|
||||
double BiCubic0(double x)
|
||||
{
|
||||
return BiCubic(x, 0);
|
||||
}
|
||||
|
||||
double BiCubic1(double x)
|
||||
{
|
||||
return BiCubic(x, -0.25);
|
||||
}
|
||||
|
||||
double BiCubic2(double x)
|
||||
{
|
||||
return BiCubic(x, -0.5);
|
||||
}
|
||||
|
||||
double BiCubic3(double x)
|
||||
{
|
||||
return BiCubic(x, -1.0);
|
||||
}
|
||||
|
||||
double BiCubic4(double x)
|
||||
{
|
||||
return BiCubic(x, -1.5);
|
||||
}
|
||||
|
||||
double BiCubic_(double x, double B, double C)
|
||||
{
|
||||
x = fabs(x);
|
||||
double x2 = x * x;
|
||||
double x3 = x * x * x;
|
||||
return
|
||||
1 / 6.0 * (x < 1 ? (12 - 9*B - 6*C) * x3 + (-18 + 12*B + 6*C) * x2 + (6 - 2*B) :
|
||||
x < 2 ? (-B - 6*C) * x3 + (6*B + 30*C) * x2 + (-12*B - 48*C) *x + (8*B + 24*C) :
|
||||
0);
|
||||
}
|
||||
|
||||
double BiCubic_Bspline(double x)
|
||||
{
|
||||
return BiCubic_(x, 1, 0);
|
||||
}
|
||||
|
||||
double BiCubic_Mitchell(double x)
|
||||
{
|
||||
return BiCubic_(x, 1 / 3.0, 1 / 3.0);
|
||||
}
|
||||
|
||||
double BiCubic_CatmullRom(double x)
|
||||
{
|
||||
return BiCubic_(x, 0, 1 / 2);
|
||||
}
|
||||
|
||||
double Nearest(double x)
|
||||
{
|
||||
return x >= -0.5 && x <= 0.5;
|
||||
|
|
@ -43,8 +102,7 @@ double LinearKernel(double x)
|
|||
|
||||
double SimpleKernel(double x)
|
||||
{
|
||||
x = fabs(x);
|
||||
return x < 1.5 ? 1 : 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
double MagicKernel(double x)
|
||||
|
|
@ -55,6 +113,14 @@ double MagicKernel(double x)
|
|||
0;
|
||||
}
|
||||
|
||||
double BSpline(double x)
|
||||
{
|
||||
x = fabs(x);
|
||||
return x <= 1 ? (x * x * x) / 2 - x * x + 2.0 / 3 :
|
||||
x <= 2 ? -(x * x * x) / 6 + x * x - 2 * x + 4.0 / 3 :
|
||||
0;
|
||||
}
|
||||
|
||||
double Lanczos(double x, int a)
|
||||
{
|
||||
x = fabs(x);
|
||||
|
|
@ -90,7 +156,7 @@ struct MyApp : TopWindow {
|
|||
|
||||
DropList kernel;
|
||||
DropList method;
|
||||
Option fast;
|
||||
DropList fn;
|
||||
|
||||
void Paint(Draw& w) {
|
||||
w.DrawRect(GetSize(), LtGray());
|
||||
|
|
@ -99,19 +165,36 @@ struct MyApp : TopWindow {
|
|||
|
||||
Image (*rescale)(const Image& _img, int cx, int cy, double (*kernel)(double x), int a, int method);
|
||||
|
||||
if(fast)
|
||||
if(~fn == 0)
|
||||
rescale = RescaleWithKernelE;
|
||||
else
|
||||
if(~fn == 1)
|
||||
rescale = RescaleWithKernel;
|
||||
else
|
||||
rescale = RescaleWithKernelE;
|
||||
rescale = RescaleWithKernel2;
|
||||
|
||||
double (*k)(double) = (double (*)(double))(int64)~kernel;
|
||||
int ka = k == Lanczos3 ? 3 : k == Lanczos4 ? 4 : k == Lanczos5 ? 5 : k == LinearKernel ? 1 : 2;
|
||||
|
||||
TimeStop tm;
|
||||
|
||||
for(int i = 0; i < TestImg().GetCount(); i++) {
|
||||
w.DrawImage(250 * i, 0, TestImg().Get(i));
|
||||
w.DrawImage(250 * i, 200, rescale(TestImg().Get(i), 84, 84, k, ka, ~method));
|
||||
w.DrawImage(250 * i + 84, 200, rescale(TestImg().Get(i), 42, 42, k, ka, ~method));
|
||||
w.DrawImage(250 * i + 84 + 52, 200, rescale(TestImg().Get(i), 21, 21, k, ka, ~method));
|
||||
w.DrawImage(250 * i + 84 + 52 + 31, 200, rescale(TestImg().Get(i), 10, 10, k, ka, ~method));
|
||||
w.DrawImage(250 * i + 84 + 52 + 31 + 20, 200, rescale(TestImg().Get(i), 5, 5, k, ka, ~method));
|
||||
w.DrawImage(250 * i + 84 + 52 + 31 + 20 + 20, 200, rescale(TestImg().Get(i), 1, 1, k, ka, ~method));
|
||||
w.DrawImage(250 * i, 300, rescale(TestImg().Get(i), 250, 250, k, ka, ~method));
|
||||
w.DrawImage(250 * i, GetSize().cy - 250 - 84 - 20, Rescale(TestImg().Get(i), 84, 84));
|
||||
w.DrawImage(250 * i, GetSize().cy - 250 - 20, Rescale(TestImg().Get(i), 250, 250));
|
||||
}
|
||||
|
||||
w.DrawImage(GetSize().cx - 400, 100, rescale(TestImg::img2(), 400, 400, k, ka, ~method));
|
||||
w.DrawImage(GetSize().cx - 400, 500, rescale(TestImg::img3(), 400, 400, k, ka, ~method));
|
||||
|
||||
w.DrawText(0, GetSize().cy - 20, String().Cat() << "Elapsed " << tm);
|
||||
|
||||
/*
|
||||
w.DrawImage(0, 0, RescaleBicubic2(rings, 180, 180, k, ka, expand));
|
||||
|
|
@ -167,8 +250,19 @@ struct MyApp : TopWindow {
|
|||
kernel.Add((int64)Nearest, "Nearest");
|
||||
kernel.Add((int64)SimpleKernel, "Simple");
|
||||
kernel.Add((int64)LinearKernel, "Linear");
|
||||
kernel.Add((int64)BiCubicKernel2, "BiCubic");
|
||||
kernel.Add((int64)BiCubicKernel2, "BiCubic old");
|
||||
kernel.Add((int64)BiCubic0, "BiCubic0");
|
||||
kernel.Add((int64)BiCubic1, "BiCubic1");
|
||||
kernel.Add((int64)BiCubic2, "BiCubic2");
|
||||
kernel.Add((int64)BiCubic3, "BiCubic3");
|
||||
kernel.Add((int64)BiCubic4, "BiCubic4");
|
||||
kernel.Add((int64)MagicKernel, "Magic");
|
||||
kernel.Add((int64)BSpline, "BSpline");
|
||||
|
||||
kernel.Add((int64)BiCubic_Bspline, "BiCubic_Bspline");
|
||||
kernel.Add((int64)BiCubic_Mitchell, "BiCubic_Mitchell");
|
||||
kernel.Add((int64)BiCubic_CatmullRom, "BiCubic_CatmullRom");
|
||||
|
||||
kernel.Add((int64)Lanczos2, "Lanczos2");
|
||||
kernel.Add((int64)Lanczos3, "Lanczos3");
|
||||
kernel.Add((int64)Lanczos4, "Lanczos4");
|
||||
|
|
@ -182,12 +276,14 @@ struct MyApp : TopWindow {
|
|||
method.Add(DOWNSCALE_WIDE, "Wide");
|
||||
method <<= THISBACK(Sync);
|
||||
Add(method.TopPos(0, STDSIZE).RightPos(200, 200));
|
||||
method <<= DOWNSCALE_SIMPLE;
|
||||
method <<= DOWNSCALE_WIDE;
|
||||
|
||||
Add(fast.TopPos(0, STDSIZE).RightPos(400, 200));
|
||||
fast <<= THISBACK(Sync);
|
||||
fast.SetLabel("Integer");
|
||||
fast <<= true;
|
||||
fn.Add(0, "FP");
|
||||
fn.Add(1, "Integer");
|
||||
fn.Add(2, "Optimized");
|
||||
Add(fn.TopPos(0, STDSIZE).RightPos(400, 200));
|
||||
fn <<= THISBACK(Sync);
|
||||
fn <<= 2;
|
||||
|
||||
Maximize();
|
||||
}
|
||||
|
|
@ -195,18 +291,21 @@ struct MyApp : TopWindow {
|
|||
|
||||
GUI_APP_MAIN
|
||||
{
|
||||
Image img = TestImg::img1();
|
||||
RescaleWithKernel2(img, 5, 5, LinearKernel, 1);
|
||||
|
||||
DUMP((int)Saturate255(5000));
|
||||
DUMP((int)Saturate255(500));
|
||||
DUMP((int)Saturate255(-1));
|
||||
DUMP((int)Saturate255(-10000));
|
||||
for(double x = -2; x <= 2; x += 0.1)
|
||||
LOG(x << ' ' << Lanczos2(x) << ' ' << MagicKernel(x) << ' ' << LinearKernel(x));
|
||||
RLOG(x << ' ' << Lanczos2(x) << ' ' << MagicKernel(x) << ' ' << BiCubic2(x));
|
||||
|
||||
// MyApp().Run();
|
||||
MyApp().Run();
|
||||
#ifndef _DEBUG
|
||||
Image img = TestImg::img1();
|
||||
#if 1
|
||||
for(int i = 0; i < 100; i++) {
|
||||
{
|
||||
/* {
|
||||
RTIMING("Rescale");
|
||||
Rescale(img, 84, 84);
|
||||
}
|
||||
|
|
@ -214,6 +313,18 @@ GUI_APP_MAIN
|
|||
RTIMING("Rescale Linear integer");
|
||||
RescaleWithKernel(img, 84, 84, LinearKernel, 1);
|
||||
}
|
||||
{
|
||||
RTIMING("Rescale Linear integer optimized");
|
||||
RescaleWithKernel2(img, 84, 84, LinearKernel, 1);
|
||||
}
|
||||
*/ {
|
||||
RTIMING("Rescale UP");
|
||||
Rescale(img, 300, 300);
|
||||
}
|
||||
{
|
||||
RTIMING("Rescale Linear integer optimized UP");
|
||||
RescaleWithKernel2(img, 300, 300, LinearKernel, 1);
|
||||
}
|
||||
#if 0
|
||||
{
|
||||
RTIMING("Rescale Linear double");
|
||||
|
|
@ -246,4 +357,5 @@ GUI_APP_MAIN
|
|||
#endif
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue