1
2 //
3 // This source file is part of appleseed.
4 // Visit https://appleseedhq.net/ for additional information and resources.
5 //
6 // This software is released under the MIT license.
7 //
8 // Copyright (c) 2018 Esteban Tovagliari, The appleseedhq Organization
9 //
10 // Permission is hereby granted, free of charge, to any person obtaining a copy
11 // of this software and associated documentation files (the "Software"), to deal
12 // in the Software without restriction, including without limitation the rights
13 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 // copies of the Software, and to permit persons to whom the Software is
15 // furnished to do so, subject to the following conditions:
16 //
17 // The above copyright notice and this permission notice shall be included in
18 // all copies or substantial portions of the Software.
19 //
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 // THE SOFTWARE.
27 //
28
29 // Interface header.
30 #include "denoiseraov.h"
31
32 // appleseed.renderer headers.
33 #include "renderer/global/globallogger.h"
34 #include "renderer/kernel/aov/aovaccumulator.h"
35 #include "renderer/kernel/rendering/pixelcontext.h"
36 #include "renderer/kernel/shading/shadingcomponents.h"
37 #include "renderer/kernel/shading/shadingresult.h"
38 #include "renderer/modeling/aov/aov.h"
39 #include "renderer/modeling/color/colorspace.h"
40 #include "renderer/modeling/frame/frame.h"
41
42 // appleseed.foundation headers.
43 #include "foundation/image/color.h"
44 #include "foundation/image/image.h"
45 #include "foundation/platform/defaulttimers.h"
46 #include "foundation/utility/api/apistring.h"
47 #include "foundation/utility/api/specializedapiarrays.h"
48 #include "foundation/utility/containers/dictionary.h"
49 #include "foundation/utility/stopwatch.h"
50 #include "foundation/utility/string.h"
51
52 // BCD headers.
53 #include "bcd/CovarianceMatrix.h"
54 #include "bcd/ImageIO.h"
55
56 // Boost headers.
57 #include "boost/filesystem.hpp"
58
59 // Standard headers.
60 #include <cmath>
61 #include <cstddef>
62
63 using namespace bcd;
64 using namespace foundation;
65 using namespace std;
66 namespace bf = boost::filesystem;
67
68 namespace renderer
69 {
70
71 namespace
72 {
73 //
74 // Denoiser AOV accumulator.
75 //
76
77 class DenoiserAOVAccumulator
78 : public AOVAccumulator
79 {
80 public:
DenoiserAOVAccumulator(const size_t num_bins,const float gamma,const float max_value,Deepimf & sum_accum,Deepimf & covariance_accum,Deepimf & histograms)81 DenoiserAOVAccumulator(
82 const size_t num_bins,
83 const float gamma,
84 const float max_value,
85 Deepimf& sum_accum,
86 Deepimf& covariance_accum,
87 Deepimf& histograms)
88 : m_num_bins(num_bins)
89 , m_gamma(gamma)
90 , m_rcp_gamma(1.0f / gamma)
91 , m_max_value(max_value)
92 , m_samples_channel_index(3 * num_bins)
93 , m_sum_accum(sum_accum)
94 , m_covariance_accum(covariance_accum)
95 , m_histograms(histograms)
96 {
97 }
98
on_tile_begin(const Frame & frame,const size_t tile_x,const size_t tile_y,const size_t max_spp)99 void on_tile_begin(
100 const Frame& frame,
101 const size_t tile_x,
102 const size_t tile_y,
103 const size_t max_spp) override
104 {
105 // Fetch the destination tile.
106 const CanvasProperties& props = frame.image().properties();
107 const Tile& tile = frame.image().tile(tile_x, tile_y);
108
109 // Fetch the tile bounds (inclusive).
110 m_tile_origin_x = static_cast<int>(tile_x * props.m_tile_width);
111 m_tile_origin_y = static_cast<int>(tile_y * props.m_tile_height);
112 m_tile_end_x = static_cast<int>(m_tile_origin_x + tile.get_width() - 1);
113 m_tile_end_y = static_cast<int>(m_tile_origin_y + tile.get_height() - 1);
114 }
115
on_sample_begin(const PixelContext & pixel_context)116 void on_sample_begin(
117 const PixelContext& pixel_context) override
118 {
119 m_accum.set(0.0f);
120 m_sample_count = 0;
121 }
122
on_sample_end(const PixelContext & pixel_context)123 void on_sample_end(
124 const PixelContext& pixel_context) override
125 {
126 // Ignore invalid samples.
127 if (m_sample_count == 0)
128 return;
129
130 const Vector2i& pi = pixel_context.get_pixel_coords();
131
132 // Ignore samples outside the tile.
133 if (outside_tile(pi))
134 return;
135
136 // Accumulate unpremultiplied samples.
137 m_accum.unpremultiply_in_place();
138
139 // Update the num samples channel.
140 m_histograms.get(pi.y, pi.x, static_cast<int>(m_samples_channel_index)) += 1.0f;
141
142 // Update the sum and covariance accumulator.
143 m_sum_accum.get(pi.y, pi.x, 0) += m_accum.r;
144 m_sum_accum.get(pi.y, pi.x, 1) += m_accum.g;
145 m_sum_accum.get(pi.y, pi.x, 2) += m_accum.b;
146
147 const size_t c_xx = static_cast<size_t>(ESymmetricMatrix3x3Data::e_xx);
148 const size_t c_yy = static_cast<size_t>(ESymmetricMatrix3x3Data::e_yy);
149 const size_t c_zz = static_cast<size_t>(ESymmetricMatrix3x3Data::e_zz);
150 const size_t c_yz = static_cast<size_t>(ESymmetricMatrix3x3Data::e_yz);
151 const size_t c_xz = static_cast<size_t>(ESymmetricMatrix3x3Data::e_xz);
152 const size_t c_xy = static_cast<size_t>(ESymmetricMatrix3x3Data::e_xy);
153
154 m_covariance_accum.get(pi.y, pi.x, c_xx) += m_accum.r * m_accum.r;
155 m_covariance_accum.get(pi.y, pi.x, c_yy) += m_accum.g * m_accum.g;
156 m_covariance_accum.get(pi.y, pi.x, c_zz) += m_accum.b * m_accum.b;
157 m_covariance_accum.get(pi.y, pi.x, c_yz) += m_accum.g * m_accum.b;
158 m_covariance_accum.get(pi.y, pi.x, c_xz) += m_accum.r * m_accum.b;
159 m_covariance_accum.get(pi.y, pi.x, c_xy) += m_accum.r * m_accum.g;
160
161 // Fill histogram: code from BCD's SampleAccumulator class.
162 for (size_t c = 0; c < 3; ++c)
163 {
164 const size_t start_bin = c * m_num_bins;
165 float value = m_accum[c];
166
167 // Clamp to 0.
168 value = max(value, 0.0f);
169
170 // Exponential scaling.
171 if (m_gamma > 1.0f) value = pow(value, m_rcp_gamma);
172
173 // Normalize to the maximum value.
174 if (m_max_value > 0.0f)
175 value /= m_max_value;
176
177 // Used for determining the weight to give to the sample
178 // in the highest two bins, when the sample is saturated.
179 const float sature_level_gamma = 2.0f;
180 value = min(value, sature_level_gamma);
181
182 const float bin_float_index = value * (m_num_bins - 2);
183
184 size_t floor_bin_index = truncate<size_t>(bin_float_index);
185 size_t ceil_bin_index;
186
187 float floor_bin_weight;
188 float ceil_bin_weight;
189
190 if (floor_bin_index < m_num_bins - 2)
191 {
192 // In bounds.
193 ceil_bin_index = floor_bin_index + 1;
194 ceil_bin_weight = bin_float_index - floor_bin_index;
195 floor_bin_weight = 1.0f - ceil_bin_weight;
196 }
197 else
198 {
199 // Out of bounds... v >= 1.
200 floor_bin_index = m_num_bins - 2;
201 ceil_bin_index = floor_bin_index + 1;
202 ceil_bin_weight = (value - 1.0f) / (sature_level_gamma - 1.f);
203 floor_bin_weight = 1.0f - ceil_bin_weight;
204 }
205
206 m_histograms.get(
207 pi.y,
208 pi.x,
209 static_cast<int>(start_bin + floor_bin_index)) += floor_bin_weight;
210
211 m_histograms.get(
212 pi.y,
213 pi.x,
214 static_cast<int>(start_bin + ceil_bin_index)) += ceil_bin_weight;
215 }
216 }
217
write(const PixelContext & pixel_context,const ShadingPoint & shading_point,const ShadingComponents & shading_components,const AOVComponents & aov_components,ShadingResult & shading_result)218 void write(
219 const PixelContext& pixel_context,
220 const ShadingPoint& shading_point,
221 const ShadingComponents& shading_components,
222 const AOVComponents& aov_components,
223 ShadingResult& shading_result) override
224 {
225 if (shading_result.is_main_valid())
226 {
227 // Composite over the previous sample.
228 Color4f main = shading_result.m_main;
229 main.premultiply_in_place();
230 m_accum += (1.0f - m_accum.a) * shading_result.m_main;
231 ++m_sample_count;
232 }
233 }
234
235 private:
236 Color4f m_accum;
237 size_t m_sample_count;
238
239 const size_t m_num_bins;
240 const float m_gamma;
241 const float m_rcp_gamma;
242 const float m_max_value;
243 const size_t m_samples_channel_index;
244
245 int m_tile_origin_x;
246 int m_tile_origin_y;
247 int m_tile_end_x;
248 int m_tile_end_y;
249
250 Deepimf& m_sum_accum;
251 Deepimf& m_covariance_accum;
252
253 Deepimf& m_histograms;
254
outside_tile(const Vector2i & pi) const255 bool outside_tile(const Vector2i& pi) const
256 {
257 return
258 pi.x < m_tile_origin_x ||
259 pi.y < m_tile_origin_y ||
260 pi.x > m_tile_end_x ||
261 pi.y > m_tile_end_y;
262 }
263 };
264
265 const char* DenoiserAOVModel = "denoiser_aov";
266 }
267
268
269 //
270 // Denoiser AOV class implementation.
271 //
272
273 struct DenoiserAOV::Impl
274 {
275 size_t m_num_bins;
276 float m_max_value;
277 float m_gamma;
278
279 Deepimf m_sum_accum;
280 Deepimf m_covariance_accum;
281
282 Deepimf m_histograms;
283 };
284
DenoiserAOV(const float max_hist_value,const size_t num_bins)285 DenoiserAOV::DenoiserAOV(
286 const float max_hist_value,
287 const size_t num_bins)
288 : AOV("denoiser", ParamArray())
289 , impl(new Impl())
290 {
291 impl->m_num_bins = num_bins;
292 impl->m_max_value = max_hist_value;
293 impl->m_gamma = 2.2f;
294 }
295
~DenoiserAOV()296 DenoiserAOV::~DenoiserAOV()
297 {
298 delete impl;
299 }
300
get_model() const301 const char* DenoiserAOV::get_model() const
302 {
303 return DenoiserAOVModel;
304 }
305
get_channel_count() const306 size_t DenoiserAOV::get_channel_count() const
307 {
308 return 0;
309 }
310
get_channel_names() const311 const char** DenoiserAOV::get_channel_names() const
312 {
313 return nullptr;
314 }
315
has_color_data() const316 bool DenoiserAOV::has_color_data() const
317 {
318 return false;
319 }
320
create_image(const size_t canvas_width,const size_t canvas_height,const size_t tile_width,const size_t tile_height,ImageStack & aov_images)321 void DenoiserAOV::create_image(
322 const size_t canvas_width,
323 const size_t canvas_height,
324 const size_t tile_width,
325 const size_t tile_height,
326 ImageStack& aov_images)
327 {
328 const int w = static_cast<int>(canvas_width);
329 const int h = static_cast<int>(canvas_height);
330 const int bins = static_cast<int>(impl->m_num_bins);
331
332 impl->m_sum_accum.resize(w, h, 3);
333 impl->m_covariance_accum.resize(w, h, 6);
334 impl->m_histograms.resize(w, h, 3 * bins + 1);
335
336 clear_image();
337 }
338
clear_image()339 void DenoiserAOV::clear_image()
340 {
341 impl->m_sum_accum.fill(0.0f);
342 impl->m_covariance_accum.fill(0.0f);
343 impl->m_histograms.fill(0.0f);
344 }
345
fill_empty_samples() const346 void DenoiserAOV::fill_empty_samples() const
347 {
348 const int w = impl->m_histograms.getWidth();
349 const int h = impl->m_histograms.getHeight();
350
351 const int num_bins = static_cast<int>(impl->m_num_bins);
352 const int samples_channel_index = num_bins * 3;
353
354 for (int y = 0; y < h; ++y)
355 {
356 for (int x = 0; x < w; ++x)
357 {
358 const float num_samples =
359 impl->m_histograms.get(y, x, samples_channel_index);
360
361 if (num_samples == 0.0f)
362 {
363 impl->m_histograms.get(y, x, 0) = 1.0f;
364 impl->m_histograms.get(y, x, num_bins) = 1.0f;
365 impl->m_histograms.get(y, x, num_bins * 2) = 1.0f;
366 impl->m_histograms.get(y, x, samples_channel_index) = 1.0f;
367 }
368 }
369 }
370 }
371
histograms_image() const372 const Deepimf& DenoiserAOV::histograms_image() const
373 {
374 return impl->m_histograms;
375 }
376
histograms_image()377 Deepimf& DenoiserAOV::histograms_image()
378 {
379 return impl->m_histograms;
380 }
381
covariance_image() const382 const Deepimf& DenoiserAOV::covariance_image() const
383 {
384 return impl->m_covariance_accum;
385 }
386
covariance_image()387 Deepimf& DenoiserAOV::covariance_image()
388 {
389 return impl->m_covariance_accum;
390 }
391
sum_image() const392 const Deepimf& DenoiserAOV::sum_image() const
393 {
394 return impl->m_sum_accum;
395 }
396
sum_image()397 Deepimf& DenoiserAOV::sum_image()
398 {
399 return impl->m_sum_accum;
400 }
401
extract_num_samples_image(bcd::Deepimf & num_samples_image) const402 void DenoiserAOV::extract_num_samples_image(bcd::Deepimf& num_samples_image) const
403 {
404 const int w = impl->m_histograms.getWidth();
405 const int h = impl->m_histograms.getHeight();
406 const int samples_channel_index = static_cast<int>(impl->m_num_bins * 3);
407
408 num_samples_image.resize(w, h, 1);
409
410 for (int y = 0; y < h; ++y)
411 {
412 for (int x = 0; x < w; ++x)
413 num_samples_image.get(y, x, 0) = impl->m_histograms.get(y, x, samples_channel_index);
414 }
415 }
416
compute_covariances_image(Deepimf & covariances_image) const417 void DenoiserAOV::compute_covariances_image(Deepimf& covariances_image) const
418 {
419 const int w = impl->m_covariance_accum.getWidth();
420 const int h = impl->m_covariance_accum.getHeight();
421
422 covariances_image.resize(w, h, 6);
423 covariances_image.fill(0.0f);
424
425 const int samples_channel_index = static_cast<int>(impl->m_num_bins * 3);
426
427 const size_t c_xx = static_cast<size_t>(ESymmetricMatrix3x3Data::e_xx);
428 const size_t c_yy = static_cast<size_t>(ESymmetricMatrix3x3Data::e_yy);
429 const size_t c_zz = static_cast<size_t>(ESymmetricMatrix3x3Data::e_zz);
430 const size_t c_yz = static_cast<size_t>(ESymmetricMatrix3x3Data::e_yz);
431 const size_t c_xz = static_cast<size_t>(ESymmetricMatrix3x3Data::e_xz);
432 const size_t c_xy = static_cast<size_t>(ESymmetricMatrix3x3Data::e_xy);
433
434 for (int y = 0; y < h; ++y)
435 {
436 for (int x = 0; x < w; ++x)
437 {
438 const float sample_count = impl->m_histograms.get(y, x, samples_channel_index);
439
440 if (sample_count != 0.0f)
441 {
442 const float rcp_sample_count = 1.0f / sample_count;
443 const float bias_correction_factor =
444 sample_count == 1.0f
445 ? 1.0f
446 : 1.0f / (1.0f - rcp_sample_count);
447
448 // Compute the mean.
449 float mean[3];
450 for (int k = 0; k < 3; ++k)
451 mean[k] = impl->m_sum_accum.get(y, x, k) * rcp_sample_count;
452
453 // Compute the covariances.
454 const float xx = impl->m_covariance_accum.get(y, x, c_xx);
455 const float yy = impl->m_covariance_accum.get(y, x, c_yy);
456 const float zz = impl->m_covariance_accum.get(y, x, c_zz);
457 const float yz = impl->m_covariance_accum.get(y, x, c_yz);
458 const float xz = impl->m_covariance_accum.get(y, x, c_xz);
459 const float xy = impl->m_covariance_accum.get(y, x, c_xy);
460
461 covariances_image.get(y, x, c_xx) = (xx * rcp_sample_count - mean[0] * mean[0]) * bias_correction_factor;
462 covariances_image.get(y, x, c_yy) = (yy * rcp_sample_count - mean[1] * mean[1]) * bias_correction_factor;
463 covariances_image.get(y, x, c_zz) = (zz * rcp_sample_count - mean[2] * mean[2]) * bias_correction_factor;
464 covariances_image.get(y, x, c_yz) = (yz * rcp_sample_count - mean[1] * mean[2]) * bias_correction_factor;
465 covariances_image.get(y, x, c_xz) = (xz * rcp_sample_count - mean[0] * mean[2]) * bias_correction_factor;
466 covariances_image.get(y, x, c_xy) = (xy * rcp_sample_count - mean[0] * mean[1]) * bias_correction_factor;
467 }
468 }
469 }
470 }
471
write_images(const char * file_path,const ImageAttributes & image_attributes) const472 bool DenoiserAOV::write_images(
473 const char* file_path,
474 const ImageAttributes& image_attributes) const
475 {
476 fill_empty_samples();
477
478 const bf::path boost_file_path(file_path);
479 const bf::path directory = boost_file_path.parent_path();
480 const string base_file_name = boost_file_path.stem().string();
481 const string extension = boost_file_path.extension().string();
482
483 bool success = true;
484
485 Stopwatch<DefaultWallclockTimer> stopwatch;
486
487 // Write histograms.
488 stopwatch.start();
489 const string hist_file_name = base_file_name + ".hist" + extension;
490 const string hist_file_path = (directory / hist_file_name).string();
491 if (ImageIO::writeMultiChannelsEXR(histograms_image(), hist_file_path.c_str()))
492 {
493 stopwatch.measure();
494 RENDERER_LOG_INFO(
495 "wrote image file %s for aov \"%s\" in %s.",
496 hist_file_path.c_str(),
497 get_path().c_str(),
498 pretty_time(stopwatch.get_seconds()).c_str());
499 }
500 else
501 {
502 RENDERER_LOG_ERROR(
503 "failed to write image file %s for aov \"%s\".",
504 hist_file_path.c_str(),
505 get_path().c_str());
506 success = false;
507 }
508
509 // Compute covariances image.
510 Deepimf covariances_image;
511 compute_covariances_image(covariances_image);
512
513 // Write covariances image.
514 stopwatch.start();
515 const string cov_file_name = base_file_name + ".cov" + extension;
516 const string cov_file_path = (directory / cov_file_name).string();
517 if (ImageIO::writeMultiChannelsEXR(covariances_image, cov_file_path.c_str()))
518 {
519 stopwatch.measure();
520 RENDERER_LOG_INFO(
521 "wrote image file %s for aov \"%s\" in %s.",
522 cov_file_path.c_str(),
523 get_path().c_str(),
524 pretty_time(stopwatch.get_seconds()).c_str());
525 }
526 else
527 {
528 RENDERER_LOG_ERROR(
529 "failed to write image file %s for aov \"%s\".",
530 cov_file_path.c_str(),
531 get_path().c_str());
532 success = false;
533 }
534
535 return success;
536 }
537
create_accumulator() const538 auto_release_ptr<AOVAccumulator> DenoiserAOV::create_accumulator() const
539 {
540 return auto_release_ptr<AOVAccumulator>(
541 new DenoiserAOVAccumulator(
542 impl->m_num_bins,
543 impl->m_gamma,
544 impl->m_max_value,
545 impl->m_sum_accum,
546 impl->m_covariance_accum,
547 impl->m_histograms));
548 }
549
550
551 //
552 // DenoiserAOVFactory class implementation.
553 //
554
create(const float max_hist_value,const size_t num_bins)555 auto_release_ptr<DenoiserAOV> DenoiserAOVFactory::create(
556 const float max_hist_value,
557 const size_t num_bins)
558 {
559 return
560 auto_release_ptr<DenoiserAOV>(
561 new DenoiserAOV(max_hist_value, num_bins));
562 }
563
564 } // namespace renderer
565