1 #include "gui/ImageTransform.h"
2 #include "thread/ThreadLocal.h"
3 
4 NAMESPACE_SPH_BEGIN
5 
interpolate(const Bitmap<Rgba> & bitmap,const float x,const float y)6 Rgba interpolate(const Bitmap<Rgba>& bitmap, const float x, const float y) {
7     const Pixel size = bitmap.size();
8     const Vector uvw =
9         clamp(Vector(x, y, 0._f), Vector(0._f), Vector(size.x - 1, size.y - 1, 0._f) - Vector(EPS));
10     const Size u1 = int(uvw[X]);
11     const Size v1 = int(uvw[Y]);
12     const Size u2 = u1 + 1;
13     const Size v2 = v1 + 1;
14     const float a = float(uvw[X] - u1);
15     const float b = float(uvw[Y] - v1);
16     SPH_ASSERT(a >= 0.f && a <= 1.f, a);
17     SPH_ASSERT(b >= 0.f && b <= 1.f, b);
18 
19     return Rgba(bitmap[Pixel(u1, v1)]) * (1.f - a) * (1.f - b) +
20            Rgba(bitmap[Pixel(u2, v1)]) * a * (1.f - b) + //
21            Rgba(bitmap[Pixel(u1, v2)]) * (1.f - a) * b + //
22            Rgba(bitmap[Pixel(u2, v2)]) * a * b;
23 }
24 
resize(const Bitmap<Rgba> & input,const Pixel size)25 Bitmap<Rgba> resize(const Bitmap<Rgba>& input, const Pixel size) {
26     const float scaleX = float(input.size().x) / size.x;
27     const float scaleY = float(input.size().y) / size.y;
28     if (min(scaleX, scaleY) > 2) {
29         // first do area-based scaling to 1/2 (and possibly recursively more)
30         Bitmap<Rgba> half(input.size() / 2);
31         for (int y = 0; y < half.size().y; ++y) {
32             for (int x = 0; x < half.size().x; ++x) {
33                 const int x1 = 2 * x;
34                 const int y1 = 2 * y;
35                 half(x, y) =
36                     (input(x1, y1) + input(x1 + 1, y1) + input(x1, y1 + 1) + input(x1 + 1, y1 + 1)) / 4.f;
37             }
38         }
39         return resize(half, size);
40     } else {
41         Bitmap<Rgba> resized(size);
42         for (int y = 0; y < size.y; ++y) {
43             for (int x = 0; x < size.x; ++x) {
44                 resized(x, y) = interpolate(input, scaleX * x, scaleY * y);
45             }
46         }
47         return resized;
48     }
49 }
50 
detectEdges(const Bitmap<Rgba> & input)51 Bitmap<float> detectEdges(const Bitmap<Rgba>& input) {
52     Bitmap<float> disc(input.size());
53     Rectangle rect(Pixel(0, 0), input.size() - Pixel(1, 1));
54     for (int y = 0; y < input.size().y; ++y) {
55         for (int x = 0; x < input.size().x; ++x) {
56             Rectangle patch = rect.intersect(Rectangle::window(Pixel(x, y), 1));
57             const float intensity2 = input(x, y).intensity();
58             float maxDiff = 0.f;
59             for (int y1 : patch.rowRange()) {
60                 for (int x1 : patch.colRange()) {
61                     const float intensity1 = input(x1, y1).intensity();
62                     maxDiff = max(maxDiff, sqr(intensity1 - intensity2));
63                 }
64             }
65             disc(x, y) = maxDiff;
66         }
67     }
68     return disc;
69 }
70 
71 template <typename TFilter>
filter(IScheduler & scheduler,const Bitmap<Rgba> & input,const Size radius,const TFilter & func)72 Bitmap<Rgba> filter(IScheduler& scheduler,
73     const Bitmap<Rgba>& input,
74     const Size radius,
75     const TFilter& func) {
76     Bitmap<Rgba> result(input.size());
77     Rectangle rect(Pixel(0, 0), input.size() - Pixel(1, 1));
78     parallelFor(scheduler, 0, input.size().y, 1, [&](const Size y) {
79         for (int x = 0; x < input.size().x; ++x) {
80             Rgba sum = Rgba::black();
81             float weight = 0.f;
82             Rectangle window = rect.intersect(Rectangle::window(Pixel(x, y), radius));
83             for (int y1 : window.rowRange()) {
84                 for (int x1 : window.colRange()) {
85                     const float w = func(Pixel(x, y), Pixel(x1, y1));
86                     SPH_ASSERT(isReal(w));
87                     sum += input(x1, y1) * w;
88                     weight += w;
89                 }
90             }
91             if (weight > 0) {
92                 SPH_ASSERT(isReal(sum.r()) && isReal(sum.g()) && isReal(sum.b()));
93                 SPH_ASSERT(weight > 0.f, weight);
94                 result(x, y) = sum / weight;
95             } else {
96                 result(x, y) = Rgba::black();
97             }
98         }
99     });
100     return result;
101 }
102 
gaussianBlur(IScheduler & scheduler,const Bitmap<Rgba> & input,const int radius)103 Bitmap<Rgba> gaussianBlur(IScheduler& scheduler, const Bitmap<Rgba>& input, const int radius) {
104     const float sigma = radius / 4.f;
105     const float norm = 1.f / (2.f * sqr(sigma));
106     Array<float> weights(2 * radius + 1);
107     float weightSum = 0.f;
108     for (Size i = 0; i < weights.size(); ++i) {
109         weights[i] = exp(-sqr(int(i) - radius) * norm);
110         weightSum += weights[i];
111     }
112     SPH_ASSERT(weightSum > 0._f);
113     for (float& w : weights) {
114         w /= weightSum;
115     }
116     // horizontal blur
117     Bitmap<Rgba> blurred(input.size());
118     parallelFor(scheduler, 0, input.size().y, 1, [&](const int y) {
119         for (int x = 0; x < radius; ++x) {
120             Rgba color = Rgba::black();
121             for (int dx = -radius; dx <= radius; ++dx) {
122                 const int x1 = abs(x + dx);
123                 color += input(x1, y) * weights[dx + radius];
124             }
125             SPH_ASSERT(isReal(color));
126             blurred(x, y) = color;
127         }
128         for (int x = radius; x < input.size().x - radius; ++x) {
129             Rgba color = Rgba::black();
130             for (int dx = -radius; dx <= radius; ++dx) {
131                 color += input(x + dx, y) * weights[dx + radius];
132             }
133             SPH_ASSERT(isReal(color));
134             blurred(x, y) = color;
135         }
136         for (int x = input.size().x - radius; x < input.size().x; ++x) {
137             Rgba color = Rgba::black();
138             for (int dx = -radius; dx <= radius; ++dx) {
139                 int x1 = x + dx;
140                 if (x1 >= input.size().x) {
141                     x1 -= x1 - input.size().x + 1;
142                 }
143                 color += input(x1, y) * weights[dx + radius];
144             }
145             SPH_ASSERT(isReal(color));
146             blurred(x, y) = color;
147         }
148     });
149     // vertical blur
150     ThreadLocal<Array<Rgba>> columns(scheduler, input.size().y);
151     parallelFor(scheduler, columns, 0, input.size().x, 1, [&](const int x, Array<Rgba>& column) {
152         for (int y = 0; y < input.size().y; ++y) {
153             column[y] = blurred(x, y);
154         }
155         for (int y = 0; y < radius; ++y) {
156             Rgba color = Rgba::black();
157             for (int dy = -radius; dy <= radius; ++dy) {
158                 const int y1 = abs(y + dy);
159                 color += column[y1] * weights[dy + radius];
160             }
161             SPH_ASSERT(isReal(color));
162             blurred(x, y) = color;
163         }
164         for (int y = radius; y < input.size().y - radius; ++y) {
165             Rgba color = Rgba::black();
166             for (int dy = -radius; dy <= radius; ++dy) {
167                 color += column[y + dy] * weights[dy + radius];
168             }
169             SPH_ASSERT(isReal(color));
170             blurred(x, y) = color;
171         }
172         for (int y = input.size().y - radius; y < input.size().y; ++y) {
173             Rgba color = Rgba::black();
174             for (int dy = -radius; dy <= radius; ++dy) {
175                 int y1 = y + dy;
176                 if (y1 >= input.size().y) {
177                     y1 -= y1 - input.size().y + 1;
178                 }
179                 color += column[y1] * weights[dy + radius];
180             }
181             SPH_ASSERT(isReal(color));
182             blurred(x, y) = color;
183         }
184     });
185     return blurred;
186 }
187 
bloomEffect(IScheduler & scheduler,const Bitmap<Rgba> & input,const int radius,const float magnitude,const float brightnessThreshold)188 Bitmap<Rgba> bloomEffect(IScheduler& scheduler,
189     const Bitmap<Rgba>& input,
190     const int radius,
191     const float magnitude,
192     const float brightnessThreshold) {
193     Bitmap<Rgba> brightPixels =
194         transform(input, [brightnessThreshold](const Pixel UNUSED(p), const Rgba& color) {
195             return color.intensity() > brightnessThreshold ? color : Rgba::black();
196         });
197     Bitmap<Rgba> bloom = gaussianBlur(scheduler, brightPixels, radius);
198     for (int y = 0; y < input.size().y; ++y) {
199         for (int x = 0; x < input.size().x; ++x) {
200             const Rgba& in = input(x, y);
201             Rgba& b = bloom(x, y);
202             b = in + b * magnitude;
203             SPH_ASSERT(isReal(b), b.r(), b.g(), b.b());
204         }
205     }
206     return bloom;
207 }
208 
denoise(IScheduler & scheduler,const Bitmap<Rgba> & input,const DenoiserParams & params)209 Bitmap<Rgba> denoise(IScheduler& scheduler, const Bitmap<Rgba>& input, const DenoiserParams& params) {
210     Rectangle rect(Pixel(0, 0), input.size() - Pixel(1, 1));
211     const float norm = 1.f / (2.f * sqr(params.sigma));
212     return filter(scheduler, input, params.filterRadius, [&](const Pixel& p1, const Pixel& p2) {
213         Rectangle patch = rect.intersect(Rectangle::window(p2, params.patchRadius));
214         float distSqr = 0.f;
215         int count = 0;
216         for (int y : patch.rowRange()) {
217             for (int x : patch.colRange()) {
218                 const Pixel pp2(x, y);
219                 const Pixel pp1(pp2 - p2 + p1);
220                 if (!rect.contains(pp1)) {
221                     continue;
222                 }
223                 SPH_ASSERT(rect.contains(pp2));
224 
225                 const Rgba v1 = input[pp1];
226                 const Rgba v2 = input[pp2];
227                 distSqr += sqr(v1.r() - v2.r()) + sqr(v1.g() - v2.g()) + sqr(v1.b() - v2.b());
228                 count++;
229             }
230         }
231         SPH_ASSERT(count > 0);
232         SPH_ASSERT(distSqr < LARGE, distSqr);
233         distSqr /= 3 * count;
234         return exp(-min(distSqr * norm, 8.f));
235     });
236 }
237 
238 constexpr float DISCONTINUITY_WEIGHT = 1.e-3f;
239 
denoiseLowFrequency(IScheduler & scheduler,const Bitmap<Rgba> & input,const DenoiserParams & params,const Size levels)240 Bitmap<Rgba> denoiseLowFrequency(IScheduler& scheduler,
241     const Bitmap<Rgba>& input,
242     const DenoiserParams& params,
243     const Size levels) {
244     Bitmap<Rgba> small = resize(input, input.size() / 2);
245     Bitmap<Rgba> denoised = denoise(scheduler, small, params);
246     if (levels > 1) {
247         DenoiserParams levelParams = params;
248         levelParams.sigma *= 0.5f;
249         denoised = denoiseLowFrequency(scheduler, denoised, levelParams, levels - 1);
250     }
251     Bitmap<Rgba> smallUpscaled = resize(small, input.size());
252     Bitmap<Rgba> denoisedUpscaled = resize(denoised, input.size());
253 
254     // add high-frequency details
255     Bitmap<float> disc = detectEdges(smallUpscaled);
256     constexpr float norm = 1.f / DISCONTINUITY_WEIGHT;
257     for (int y = 0; y < input.size().y; ++y) {
258         for (int x = 0; x < input.size().x; ++x) {
259             const Pixel p(x, y);
260             const Rgba c_0 = input[p];
261             const Rgba c_f = denoisedUpscaled[p] + (input[p] - smallUpscaled[p]);
262             const float w = exp(-norm * disc[p]);
263             SPH_ASSERT(isReal(c_0) && isReal(c_f) && isReal(w));
264             denoisedUpscaled[p] = lerp(c_0, c_f, w);
265         }
266     }
267     return denoisedUpscaled;
268 }
269 
270 NAMESPACE_SPH_END
271