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