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