Cycles: add animation denoising Python operator.
authorLukas Stockner <lukas.stockner@freenet.de>
Wed, 6 Feb 2019 11:57:10 +0000 (12:57 +0100)
committerBrecht Van Lommel <brechtvanlommel@gmail.com>
Mon, 11 Feb 2019 12:32:54 +0000 (13:32 +0100)
This adds a cycles.denoise_animation operator, which denoises an animation
sequence or individual file. Renders must be saved as multilayer EXR files
with denoising data passes.

By default file path and frame range come from the current scene, and EXR
files are denoised in-place. Alternatively, a different input and/or output
file path can be provided.

Denoising settings come from the current view layer. Renders can be denoised
again with different settings, as the original noisy image is preserved along
with other passes and metadata.

There is no user interface yet for this feature, that comes later.

Code by Lukas with modifications by Brecht. This feature was originally
developed for Tangent Animation, thanks for the support!

16 files changed:
intern/cycles/blender/CMakeLists.txt
intern/cycles/blender/addon/__init__.py
intern/cycles/blender/addon/operators.py [new file with mode: 0644]
intern/cycles/blender/addon/properties.py
intern/cycles/blender/addon/ui.py
intern/cycles/blender/blender_python.cpp
intern/cycles/blender/blender_session.cpp
intern/cycles/device/device_denoising.cpp
intern/cycles/device/device_task.h
intern/cycles/render/CMakeLists.txt
intern/cycles/render/denoising.cpp [new file with mode: 0644]
intern/cycles/render/denoising.h [new file with mode: 0644]
intern/cycles/render/session.cpp
intern/cycles/render/session.h
intern/cycles/util/util_system.cpp
intern/cycles/util/util_system.h

index 769bc78..84e2690 100644 (file)
@@ -41,6 +41,7 @@ set(SRC
 set(ADDON_FILES
        addon/__init__.py
        addon/engine.py
+       addon/operators.py
        addon/osl.py
        addon/presets.py
        addon/properties.py
index 0381262..1f14853 100644 (file)
@@ -37,6 +37,8 @@ if "bpy" in locals():
         importlib.reload(version_update)
     if "ui" in locals():
         importlib.reload(ui)
+    if "operators" in locals():
+        importlib.reload(operators)
     if "properties" in locals():
         importlib.reload(properties)
     if "presets" in locals():
@@ -118,6 +120,7 @@ classes = (
 def register():
     from bpy.utils import register_class
     from . import ui
+    from . import operators
     from . import properties
     from . import presets
     import atexit
@@ -130,6 +133,7 @@ def register():
 
     properties.register()
     ui.register()
+    operators.register()
     presets.register()
 
     for cls in classes:
@@ -141,6 +145,7 @@ def register():
 def unregister():
     from bpy.utils import unregister_class
     from . import ui
+    from . import operators
     from . import properties
     from . import presets
     import atexit
@@ -148,6 +153,7 @@ def unregister():
     bpy.app.handlers.version_update.remove(version_update.do_versions)
 
     ui.unregister()
+    operators.unregister()
     properties.unregister()
     presets.unregister()
 
diff --git a/intern/cycles/blender/addon/operators.py b/intern/cycles/blender/addon/operators.py
new file mode 100644 (file)
index 0000000..c39aa38
--- /dev/null
@@ -0,0 +1,133 @@
+#
+# Copyright 2011-2019 Blender Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# <pep8 compliant>
+
+import bpy
+from bpy.types import Operator
+from bpy.props import StringProperty
+
+
+class CYCLES_OT_use_shading_nodes(Operator):
+    """Enable nodes on a material, world or light"""
+    bl_idname = "cycles.use_shading_nodes"
+    bl_label = "Use Nodes"
+
+    @classmethod
+    def poll(cls, context):
+        return (getattr(context, "material", False) or getattr(context, "world", False) or
+                getattr(context, "light", False))
+
+    def execute(self, context):
+        if context.material:
+            context.material.use_nodes = True
+        elif context.world:
+            context.world.use_nodes = True
+        elif context.light:
+            context.light.use_nodes = True
+
+        return {'FINISHED'}
+
+
+class CYCLES_OT_denoise_animation(Operator):
+    """Denoise rendered animation sequence using current scene and view """ \
+    """layer settings. Requires denoising data passes and output to """ \
+    """OpenEXR multilayer files"""
+    bl_idname = "cycles.denoise_animation"
+    bl_label = "Denoise Animation"
+
+    input_filepath = StringProperty(
+        name='Input Filepath',
+        description='File path for frames to denoise. If not specified, uses the render file path from the scene',
+        default='',
+        subtype='FILE_PATH')
+
+    output_filepath = StringProperty(
+        name='Output Filepath',
+        description='If not specified, renders will be denoised in-place',
+        default='',
+        subtype='FILE_PATH')
+
+    def execute(self, context):
+        import os
+
+        preferences = context.user_preferences
+        scene = context.scene
+        render_layer = scene.render.layers.active
+
+        in_filepath = self.input_filepath
+        out_filepath = self.output_filepath
+
+        if in_filepath == '':
+            in_filepath = scene.render.filepath
+        if out_filepath == '':
+            out_filepath = in_filepath
+
+        # Backup since we will overwrite the scene path temporarily
+        original_filepath = scene.render.filepath
+
+        # Expand filepaths for each frame so we match Blender render output exactly.
+        in_filepaths = []
+        out_filepaths = []
+
+        for frame in range(scene.frame_start, scene.frame_end + 1):
+            scene.render.filepath = in_filepath
+            filepath = scene.render.frame_path(frame=frame)
+            in_filepaths.append(filepath)
+
+            if not os.path.isfile(filepath):
+                scene.render.filepath = original_filepath
+                self.report({'ERROR'}, f"Frame '{filepath}' not found, animation must be complete.")
+                return {'CANCELLED'}
+
+            scene.render.filepath = out_filepath
+            filepath = scene.render.frame_path(frame=frame)
+            out_filepaths.append(filepath)
+
+        scene.render.filepath = original_filepath
+
+        # Run denoiser
+        # TODO: support cancel and progress reports.
+        import _cycles
+        try:
+            _cycles.denoise(preferences.as_pointer(),
+                            scene.as_pointer(),
+                            render_layer.as_pointer(),
+                            input=in_filepaths,
+                            output=out_filepaths)
+        except Exception as e:
+            self.report({'ERROR'}, str(e))
+            return {'FINISHED'}
+
+        self.report({'INFO'}, "Denoising completed.")
+        return {'FINISHED'}
+
+
+classes = (
+    CYCLES_OT_use_shading_nodes,
+    CYCLES_OT_denoise_animation
+)
+
+def register():
+    from bpy.utils import register_class
+    for cls in classes:
+        register_class(cls)
+
+
+def unregister():
+    from bpy.utils import unregister_class
+    for cls in classes:
+        unregister_class(cls)
index 23ab1cf..1106923 100644 (file)
@@ -1344,6 +1344,12 @@ class CyclesRenderLayerSettings(bpy.types.PropertyGroup):
             default=False,
             update=update_render_passes,
         )
+        denoising_neighbor_frames: IntProperty(
+            name="Neighbor Frames",
+            description="Number of neighboring frames to use for denoising animations (more frames produce smoother results at the cost of performance)",
+            min=0, max=7,
+            default=0,
+        )
         cls.use_pass_crypto_object = BoolProperty(
                 name="Cryptomatte Object",
                 description="Render cryptomatte object pass, for isolating objects in compositing",
index 2f1adfe..e372843 100644 (file)
@@ -22,7 +22,6 @@ import _cycles
 from bpy.types import (
     Panel,
     Menu,
-    Operator,
 )
 
 
@@ -912,27 +911,6 @@ class CYCLES_OBJECT_PT_cycles_settings(CyclesButtonsPanel, Panel):
         sub.prop(cob, "use_distance_cull")
 
 
-class CYCLES_OT_use_shading_nodes(Operator):
-    """Enable nodes on a material, world or lamp"""
-    bl_idname = "cycles.use_shading_nodes"
-    bl_label = "Use Nodes"
-
-    @classmethod
-    def poll(cls, context):
-        return (getattr(context, "material", False) or getattr(context, "world", False) or
-                getattr(context, "lamp", False))
-
-    def execute(self, context):
-        if context.material:
-            context.material.use_nodes = True
-        elif context.world:
-            context.world.use_nodes = True
-        elif context.lamp:
-            context.lamp.use_nodes = True
-
-        return {'FINISHED'}
-
-
 def find_node(material, nodetype):
     if material and material.node_tree:
         ntree = material.node_tree
@@ -1870,7 +1848,6 @@ classes = (
     CYCLES_PT_context_material,
     CYCLES_OBJECT_PT_motion_blur,
     CYCLES_OBJECT_PT_cycles_settings,
-    CYCLES_OT_use_shading_nodes,
     CYCLES_LAMP_PT_preview,
     CYCLES_LAMP_PT_lamp,
     CYCLES_LAMP_PT_nodes,
index 9dd0cd4..bf7605e 100644 (file)
 
 #include "blender/CCL_api.h"
 
+#include "blender/blender_device.h"
 #include "blender/blender_sync.h"
 #include "blender/blender_session.h"
 
+#include "render/denoising.h"
+
 #include "util/util_debug.h"
 #include "util/util_foreach.h"
 #include "util/util_logging.h"
@@ -623,6 +626,121 @@ static PyObject *opencl_disable_func(PyObject * /*self*/, PyObject * /*value*/)
 }
 #endif
 
+static bool denoise_parse_filepaths(PyObject *pyfilepaths, vector<string>& filepaths)
+{
+
+       if(PyUnicode_Check(pyfilepaths)) {
+               const char *filepath = PyUnicode_AsUTF8(pyfilepaths);
+               filepaths.push_back(filepath);
+               return true;
+       }
+
+       PyObject *sequence = PySequence_Fast(pyfilepaths, "File paths must be a string or sequence of strings");
+       if(sequence == NULL) {
+               return false;
+       }
+
+       for(Py_ssize_t i = 0; i < PySequence_Fast_GET_SIZE(sequence); i++) {
+               PyObject *item = PySequence_Fast_GET_ITEM(sequence, i);
+               const char *filepath = PyUnicode_AsUTF8(item);
+               if(filepath == NULL) {
+                       PyErr_SetString(PyExc_ValueError, "File paths must be a string or sequence of strings.");
+                       Py_DECREF(sequence);
+                       return false;
+               }
+               filepaths.push_back(filepath);
+       }
+       Py_DECREF(sequence);
+
+       return true;
+}
+
+static PyObject *denoise_func(PyObject * /*self*/, PyObject *args, PyObject *keywords)
+{
+       static const char *keyword_list[] = {"preferences", "scene", "view_layer",
+                                            "input", "output",
+                                            "tile_size", "samples", NULL};
+       PyObject *pypreferences, *pyscene, *pyrenderlayer;
+       PyObject *pyinput, *pyoutput = NULL;
+       int tile_size = 0, samples = 0;
+
+       if (!PyArg_ParseTupleAndKeywords(args, keywords, "OOOO|Oii", (char**)keyword_list,
+                                        &pypreferences, &pyscene, &pyrenderlayer,
+                                                                        &pyinput, &pyoutput,
+                                        &tile_size, &samples)) {
+               return NULL;
+       }
+
+       /* Get device specification from preferences and scene. */
+       PointerRNA preferencesptr;
+       RNA_pointer_create(NULL, &RNA_UserPreferences, (void*)PyLong_AsVoidPtr(pypreferences), &preferencesptr);
+       BL::UserPreferences b_preferences(preferencesptr);
+
+       PointerRNA sceneptr;
+       RNA_id_pointer_create((ID*)PyLong_AsVoidPtr(pyscene), &sceneptr);
+       BL::Scene b_scene(sceneptr);
+
+       DeviceInfo device = blender_device_info(b_preferences, b_scene, true);
+
+       /* Get denoising parameters from view layer. */
+       PointerRNA renderlayerptr;
+       RNA_pointer_create((ID*)PyLong_AsVoidPtr(pyscene), &RNA_SceneRenderLayer, PyLong_AsVoidPtr(pyrenderlayer), &renderlayerptr);
+       PointerRNA crenderlayer = RNA_pointer_get(&renderlayerptr, "cycles");
+
+       DenoiseParams params;
+       params.radius = get_int(crenderlayer, "denoising_radius");
+       params.strength = get_float(crenderlayer, "denoising_strength");
+       params.feature_strength = get_float(crenderlayer, "denoising_feature_strength");
+       params.relative_pca = get_boolean(crenderlayer, "denoising_relative_pca");
+       params.neighbor_frames = get_int(crenderlayer, "denoising_neighbor_frames");
+
+       /* Parse file paths list. */
+       vector<string> input, output;
+
+       if(!denoise_parse_filepaths(pyinput, input)) {
+               return NULL;
+       }
+
+       if(pyoutput) {
+               if(!denoise_parse_filepaths(pyoutput, output)) {
+                       return NULL;
+               }
+       }
+       else {
+               output = input;
+       }
+
+       if(input.empty()) {
+               PyErr_SetString(PyExc_ValueError, "No input file paths specified.");
+               return NULL;
+       }
+       if(input.size() != output.size()) {
+               PyErr_SetString(PyExc_ValueError, "Number of input and output file paths does not match.");
+               return NULL;
+       }
+
+       /* Create denoiser. */
+       Denoiser denoiser(device);
+       denoiser.params = params;
+       denoiser.input = input;
+       denoiser.output = output;
+
+       if (tile_size > 0) {
+               denoiser.tile_size = make_int2(tile_size, tile_size);
+       }
+       if (samples > 0) {
+               denoiser.samples_override = samples;
+       }
+
+       /* Run denoiser. */
+       if(!denoiser.run()) {
+               PyErr_SetString(PyExc_ValueError, denoiser.error.c_str());
+               return NULL;
+       }
+
+       Py_RETURN_NONE;
+}
+
 static PyObject *debug_flags_update_func(PyObject * /*self*/, PyObject *args)
 {
        PyObject *pyscene;
@@ -783,6 +901,9 @@ static PyMethodDef methods[] = {
        {"opencl_disable", opencl_disable_func, METH_NOARGS, ""},
 #endif
 
+       /* Standalone denoising */
+       {"denoise", (PyCFunction)denoise_func, METH_VARARGS|METH_KEYWORDS, ""},
+
        /* Debugging routines */
        {"debug_flags_update", debug_flags_update_func, METH_VARARGS, ""},
        {"debug_flags_reset", debug_flags_reset_func, METH_NOARGS, ""},
index 50ac350..43d7ff4 100644 (file)
@@ -431,10 +431,10 @@ void BlenderSession::render()
                session->params.run_denoising = run_denoising;
                session->params.full_denoising = full_denoising;
                session->params.write_denoising_passes = write_denoising_passes;
-               session->params.denoising_radius = get_int(crl, "denoising_radius");
-               session->params.denoising_strength = get_float(crl, "denoising_strength");
-               session->params.denoising_feature_strength = get_float(crl, "denoising_feature_strength");
-               session->params.denoising_relative_pca = get_boolean(crl, "denoising_relative_pca");
+               session->params.denoising.radius = get_int(crl, "denoising_radius");
+               session->params.denoising.strength = get_float(crl, "denoising_strength");
+               session->params.denoising.feature_strength = get_float(crl, "denoising_feature_strength");
+               session->params.denoising.relative_pca = get_boolean(crl, "denoising_relative_pca");
 
                scene->film->denoising_data_pass = buffer_params.denoising_data_pass;
                scene->film->denoising_clean_pass = buffer_params.denoising_clean_pass;
index 61e0ba4..1bb144e 100644 (file)
@@ -27,13 +27,13 @@ DenoisingTask::DenoisingTask(Device *device, const DeviceTask &task)
   buffer(device),
   device(device)
 {
-       radius = task.denoising_radius;
-       nlm_k_2 = powf(2.0f, lerp(-5.0f, 3.0f, task.denoising_strength));
-       if(task.denoising_relative_pca) {
-               pca_threshold = -powf(10.0f, lerp(-8.0f, 0.0f, task.denoising_feature_strength));
+       radius = task.denoising.radius;
+       nlm_k_2 = powf(2.0f, lerp(-5.0f, 3.0f, task.denoising.strength));
+       if(task.denoising.relative_pca) {
+               pca_threshold = -powf(10.0f, lerp(-8.0f, 0.0f, task.denoising.feature_strength));
        }
        else {
-               pca_threshold = powf(10.0f, lerp(-5.0f, 3.0f, task.denoising_feature_strength));
+               pca_threshold = powf(10.0f, lerp(-5.0f, 3.0f, task.denoising.feature_strength));
        }
 
        render_buffer.frame_stride = task.frame_stride;
index 2871bc5..f1fd424 100644 (file)
@@ -32,6 +32,32 @@ class RenderBuffers;
 class RenderTile;
 class Tile;
 
+class DenoiseParams {
+public:
+       /* Pixel radius for neighbouring pixels to take into account. */
+       int radius;
+       /* Controls neighbor pixel weighting for the denoising filter. */
+       float strength;
+       /* Preserve more or less detail based on feature passes. */
+       float feature_strength;
+       /* When removing pixels that don't carry information, use a relative threshold instead of an absolute one. */
+       bool relative_pca;
+       /* How many frames before and after the current center frame are included. */
+       int neighbor_frames;
+       /* Clamp the input to the range of +-1e8. Should be enough for any legitimate data. */
+       bool clamp_input;
+
+       DenoiseParams()
+       {
+               radius = 8;
+               strength = 0.5f;
+               feature_strength = 0.5f;
+               relative_pca = false;
+               neighbor_frames = 2;
+               clamp_input = true;
+       }
+};
+
 class DeviceTask : public Task {
 public:
        typedef enum { RENDER, FILM_CONVERT, SHADER } Type;
@@ -68,10 +94,7 @@ public:
        function<void(RenderTile*, Device*)> map_neighbor_tiles;
        function<void(RenderTile*, Device*)> unmap_neighbor_tiles;
 
-       int denoising_radius;
-       float denoising_strength;
-       float denoising_feature_strength;
-       bool denoising_relative_pca;
+       DenoiseParams denoising;
        bool denoising_from_render;
        vector<int> denoising_frames;
 
index c0ce736..e6afbc5 100644 (file)
@@ -16,6 +16,7 @@ set(SRC
        camera.cpp
        constant_fold.cpp
        coverage.cpp
+       denoising.cpp
        film.cpp
        graph.cpp
        image.cpp
@@ -48,6 +49,7 @@ set(SRC_HEADERS
        camera.h
        constant_fold.h
        coverage.h
+       denoising.h
        film.h
        graph.h
        image.h
diff --git a/intern/cycles/render/denoising.cpp b/intern/cycles/render/denoising.cpp
new file mode 100644 (file)
index 0000000..0016a06
--- /dev/null
@@ -0,0 +1,887 @@
+/*
+ * Copyright 2011-2018 Blender Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "render/denoising.h"
+
+#include "kernel/filter/filter_defines.h"
+
+#include "util/util_foreach.h"
+#include "util/util_map.h"
+#include "util/util_system.h"
+#include "util/util_time.h"
+
+#include <OpenImageIO/filesystem.h>
+
+CCL_NAMESPACE_BEGIN
+
+/* Utility Functions */
+
+static void print_progress(int num, int total, int frame, int num_frames)
+{
+       const char *label = "Denoise Frame ";
+       int cols = system_console_width();
+
+       cols -= strlen(label);
+
+       int len = 1;
+       for(int x = total; x > 9; x /= 10) {
+               len++;
+       }
+
+       int bars = cols - 2*len - 6;
+
+       printf("\r%s", label);
+
+       if(num_frames > 1) {
+               int frame_len = 1;
+               for(int x = num_frames - 1; x > 9; x /= 10) {
+                       frame_len++;
+               }
+               bars -= frame_len + 2;
+               printf("%*d ", frame_len, frame);
+       }
+
+       int v = int(float(num)*bars/total);
+       printf("[");
+       for(int i = 0; i < v; i++) {
+               printf("=");
+       }
+       if(v < bars) {
+               printf(">");
+       }
+       for(int i = v+1; i < bars; i++) {
+               printf(" ");
+       }
+       printf(string_printf("] %%%dd / %d", len, total).c_str(), num);
+       fflush(stdout);
+}
+
+/* Splits in at its last dot, setting suffix to the part after the dot and in to the part before it.
+ * Returns whether a dot was found. */
+static bool split_last_dot(string &in, string &suffix)
+{
+       size_t pos = in.rfind(".");
+       if(pos == string::npos) {
+               return false;
+       }
+       suffix = in.substr(pos+1);
+       in = in.substr(0, pos);
+       return true;
+}
+
+/* Separate channel names as generated by Blender.
+ * If views is true:
+ *   Inputs are expected in the form RenderLayer.Pass.View.Channel, sets renderlayer to "RenderLayer.View"
+ * Otherwise:
+ *   Inputs are expected in the form RenderLayer.Pass.Channel */
+static bool parse_channel_name(string name, string &renderlayer, string &pass, string &channel, bool multiview_channels)
+{
+       if(!split_last_dot(name, channel)) {
+               return false;
+       }
+       string view;
+       if(multiview_channels && !split_last_dot(name, view)) {
+               return false;
+       }
+       if(!split_last_dot(name, pass)) {
+               return false;
+       }
+       renderlayer = name;
+
+       if(multiview_channels) {
+               renderlayer += "." + view;
+       }
+
+       return true;
+}
+
+/* Channel Mapping */
+
+struct ChannelMapping {
+       int channel;
+       string name;
+};
+
+static void fill_mapping(vector<ChannelMapping> &map, int pos, string name, string channels)
+{
+       for(const char *chan = channels.c_str(); *chan; chan++) {
+               map.push_back({pos++, name + "." + *chan});
+       }
+}
+
+static const int INPUT_NUM_CHANNELS = 15;
+static vector<ChannelMapping> init_input_channels()
+{
+       vector<ChannelMapping> map;
+       fill_mapping(map, 0, "Denoising Depth", "Z");
+       fill_mapping(map, 1, "Denoising Normal", "XYZ");
+       fill_mapping(map, 4, "Denoising Shadowing", "X");
+       fill_mapping(map, 5, "Denoising Albedo", "RGB");
+       fill_mapping(map, 8, "Noisy Image", "RGB");
+       fill_mapping(map, 11, "Denoising Variance", "RGB");
+       fill_mapping(map, 14, "Denoising Intensity", "X");
+       return map;
+}
+
+static const int OUTPUT_NUM_CHANNELS = 3;
+static vector<ChannelMapping> init_output_channels()
+{
+       vector<ChannelMapping> map;
+       fill_mapping(map, 0, "Combined", "RGB");
+       return map;
+}
+
+static const vector<ChannelMapping> input_channels = init_input_channels();
+static const vector<ChannelMapping> output_channels = init_output_channels();
+
+/* Renderlayer Handling */
+
+bool DenoiseImageLayer::detect_denoising_channels()
+{
+       /* Map device input to image channels. */
+       input_to_image_channel.clear();
+       input_to_image_channel.resize(INPUT_NUM_CHANNELS, -1);
+
+       foreach(const ChannelMapping& mapping, input_channels) {
+               vector<string>::iterator i = find(channels.begin(), channels.end(), mapping.name);
+               if(i == channels.end()) {
+                       return false;
+               }
+
+               size_t input_channel = mapping.channel;
+               size_t layer_channel = i - channels.begin();
+               input_to_image_channel[input_channel] = layer_to_image_channel[layer_channel];
+       }
+
+       /* Map device output to image channels. */
+       output_to_image_channel.clear();
+       output_to_image_channel.resize(OUTPUT_NUM_CHANNELS, -1);
+
+       foreach(const ChannelMapping& mapping, output_channels) {
+               vector<string>::iterator i = find(channels.begin(), channels.end(), mapping.name);
+               if(i == channels.end()) {
+                       return false;
+               }
+
+               size_t output_channel = mapping.channel;
+               size_t layer_channel = i - channels.begin();
+               output_to_image_channel[output_channel] = layer_to_image_channel[layer_channel];
+       }
+
+       /* Check that all buffer channels are correctly set. */
+       for(int i = 0; i < INPUT_NUM_CHANNELS; i++) {
+               assert(input_to_image_channel[i] >= 0);
+       }
+       for(int i = 0; i < OUTPUT_NUM_CHANNELS; i++) {
+               assert(output_to_image_channel[i] >= 0);
+       }
+
+       return true;
+}
+
+bool DenoiseImageLayer::match_channels(int neighbor,
+                                       const std::vector<string> &channelnames,
+                                       const std::vector<string> &neighbor_channelnames)
+{
+       neighbor_input_to_image_channel.resize(neighbor + 1);
+       vector<int>& mapping = neighbor_input_to_image_channel[neighbor];
+
+       assert(mapping.size() == 0);
+       mapping.resize(input_to_image_channel.size(), -1);
+
+       for(int i = 0; i < input_to_image_channel.size(); i++) {
+               const string& channel = channelnames[input_to_image_channel[i]];
+               std::vector<string>::const_iterator frame_channel = find(neighbor_channelnames.begin(), neighbor_channelnames.end(), channel);
+
+               if(frame_channel == neighbor_channelnames.end()) {
+                       return false;
+               }
+
+               mapping[i] = frame_channel - neighbor_channelnames.begin();
+       }
+
+       return true;
+}
+
+/* Denoise Task */
+
+DenoiseTask::DenoiseTask(Device *device, Denoiser *denoiser, int frame, const vector<int>& neighbor_frames)
+: denoiser(denoiser),
+  device(device),
+  frame(frame),
+  neighbor_frames(neighbor_frames),
+  current_layer(0),
+  input_pixels(device, "filter input buffer", MEM_READ_ONLY),
+  num_tiles(0)
+{
+       image.samples = denoiser->samples_override;
+}
+
+DenoiseTask::~DenoiseTask()
+{
+       free();
+}
+
+/* Device callbacks */
+
+bool DenoiseTask::acquire_tile(Device *device, Device *tile_device, RenderTile &tile)
+{
+       thread_scoped_lock tile_lock(tiles_mutex);
+
+       if(tiles.empty()) {
+               return false;
+       }
+
+       tile = tiles.front();
+       tiles.pop_front();
+
+       device->map_tile(tile_device, tile);
+
+       print_progress(num_tiles - tiles.size(), num_tiles, frame, denoiser->num_frames);
+
+       return true;
+}
+
+/* Mapping tiles is required for regular rendering since each tile has its separate memory
+ * which may be allocated on a different device.
+ * For standalone denoising, there is a single memory that is present on all devices, so the only
+ * thing that needs to be done here is to specify the surrounding tile geometry.
+ *
+ * However, since there is only one large memory, the denoised result has to be written to
+ * a different buffer to avoid having to copy an entire horizontal slice of the image. */
+void DenoiseTask::map_neighboring_tiles(RenderTile *tiles, Device *tile_device)
+{
+       for(int i = 0; i < 9; i++) {
+               if(i == 4) {
+                       continue;
+               }
+
+               int dx = (i%3)-1;
+               int dy = (i/3)-1;
+               tiles[i].x = clamp(tiles[4].x +  dx   *denoiser->tile_size.x, 0, image.width);
+               tiles[i].w = clamp(tiles[4].x + (dx+1)*denoiser->tile_size.x, 0, image.width) - tiles[i].x;
+               tiles[i].y = clamp(tiles[4].y +  dy   *denoiser->tile_size.y, 0, image.height);
+               tiles[i].h = clamp(tiles[4].y + (dy+1)*denoiser->tile_size.y, 0, image.height) - tiles[i].y;
+
+               tiles[i].buffer = tiles[4].buffer;
+               tiles[i].offset = tiles[4].offset;
+               tiles[i].stride = image.width;
+       }
+
+       device_vector<float> *output_mem = new device_vector<float>(tile_device, "denoising_output", MEM_READ_WRITE);
+       output_mem->alloc(OUTPUT_NUM_CHANNELS*tiles[4].w*tiles[4].h);
+       output_mem->zero_to_device();
+
+       tiles[9] = tiles[4];
+       tiles[9].buffer = output_mem->device_pointer;
+       tiles[9].stride = tiles[9].w;
+       tiles[9].offset -= tiles[9].x + tiles[9].y*tiles[9].stride;
+
+       thread_scoped_lock output_lock(output_mutex);
+       assert(output_pixels.count(tiles[4].tile_index) == 0);
+       output_pixels[tiles[9].tile_index] = output_mem;
+}
+
+void DenoiseTask::unmap_neighboring_tiles(RenderTile *tiles)
+{
+       thread_scoped_lock output_lock(output_mutex);
+       assert(output_pixels.count(tiles[4].tile_index) == 1);
+       device_vector<float> *output_mem = output_pixels[tiles[9].tile_index];
+       output_pixels.erase(tiles[4].tile_index);
+       output_lock.unlock();
+
+       output_mem->copy_from_device(0, OUTPUT_NUM_CHANNELS*tiles[9].w, tiles[9].h);
+
+       float *result = output_mem->data();
+       float *out = &image.pixels[image.num_channels*(tiles[9].y*image.width + tiles[9].x)];
+
+       const DenoiseImageLayer& layer = image.layers[current_layer];
+       const int *output_to_image_channel = layer.output_to_image_channel.data();
+
+       for(int y = 0; y < tiles[9].h; y++) {
+               for(int x = 0; x < tiles[9].w; x++, result += OUTPUT_NUM_CHANNELS) {
+                       for(int i = 0; i < OUTPUT_NUM_CHANNELS; i++) {
+                               out[image.num_channels*x + output_to_image_channel[i]] = result[i];
+                       }
+               }
+               out += image.num_channels * image.width;
+       }
+
+       output_mem->free();
+       delete output_mem;
+}
+
+void DenoiseTask::release_tile()
+{
+}
+
+bool DenoiseTask::get_cancel()
+{
+       return false;
+}
+
+void DenoiseTask::create_task(DeviceTask& task)
+{
+       /* Callback functions. */
+       task.acquire_tile = function_bind(&DenoiseTask::acquire_tile, this, device, _1, _2);
+       task.map_neighbor_tiles = function_bind(&DenoiseTask::map_neighboring_tiles, this, _1, _2);
+       task.unmap_neighbor_tiles = function_bind(&DenoiseTask::unmap_neighboring_tiles, this, _1);
+       task.release_tile = function_bind(&DenoiseTask::release_tile, this);
+       task.get_cancel = function_bind(&DenoiseTask::get_cancel, this);
+
+       /* Denoising parameters. */
+       task.denoising = denoiser->params;
+       task.denoising_do_filter = true;
+       task.denoising_write_passes = false;
+       task.denoising_from_render = false;
+
+       task.denoising_frames.resize(neighbor_frames.size());
+       for(int i = 0; i < neighbor_frames.size(); i++) {
+               task.denoising_frames[i] = neighbor_frames[i] - frame;
+       }
+
+       /* Buffer parameters. */
+       task.pass_stride = INPUT_NUM_CHANNELS;
+       task.target_pass_stride = OUTPUT_NUM_CHANNELS;
+       task.pass_denoising_data = 0;
+       task.pass_denoising_clean = -1;
+       task.frame_stride = image.width * image.height * INPUT_NUM_CHANNELS;
+
+       /* Create tiles. */
+       thread_scoped_lock tile_lock(tiles_mutex);
+       thread_scoped_lock output_lock(output_mutex);
+
+       tiles.clear();
+       assert(output_pixels.empty());
+       output_pixels.clear();
+
+       int tiles_x = divide_up(image.width, denoiser->tile_size.x);
+       int tiles_y = divide_up(image.height, denoiser->tile_size.y);
+
+       for(int ty = 0; ty < tiles_y; ty++) {
+               for(int tx = 0; tx < tiles_x; tx++) {
+                       RenderTile tile;
+                       tile.x = tx * denoiser->tile_size.x;
+                       tile.y = ty * denoiser->tile_size.y;
+                       tile.w = min(image.width - tile.x, denoiser->tile_size.x);
+                       tile.h = min(image.height - tile.y, denoiser->tile_size.y);
+                       tile.start_sample = 0;
+                       tile.num_samples = image.layers[current_layer].samples;
+                       tile.sample = 0;
+                       tile.offset = 0;
+                       tile.stride = image.width;
+                       tile.tile_index = ty*tiles_x + tx;
+                       tile.task = RenderTile::DENOISE;
+                       tile.buffers = NULL;
+                       tile.buffer = input_pixels.device_pointer;
+                       tiles.push_back(tile);
+               }
+       }
+
+       num_tiles = tiles.size();
+}
+
+/* Denoiser Operations */
+
+bool DenoiseTask::load_input_pixels(int layer)
+{
+       int w = image.width;
+       int h = image.height;
+       int num_pixels = image.width * image.height;
+       int frame_stride = num_pixels * INPUT_NUM_CHANNELS;
+
+       /* Load center image */
+       DenoiseImageLayer& image_layer = image.layers[layer];
+
+       float *buffer_data = input_pixels.data();
+       image.read_pixels(image_layer, buffer_data);
+       buffer_data += frame_stride;
+
+       /* Load neighbor images */
+       for(int i = 0; i < image.in_neighbors.size(); i++) {
+               if(!image.read_neighbor_pixels(i, image_layer, buffer_data)) {
+                       error = "Failed to read neighbor frame pixels";
+                       return false;
+               }
+               buffer_data += frame_stride;
+       }
+
+       /* Preprocess */
+       buffer_data = input_pixels.data();
+       for(int neighbor = 0; neighbor < image.in_neighbors.size() + 1; neighbor++) {
+               /* Clamp */
+               if(denoiser->params.clamp_input) {
+                       for(int i = 0; i < num_pixels*INPUT_NUM_CHANNELS; i++) {
+                               buffer_data[i] = clamp(buffer_data[i], -1e8f, 1e8f);
+                       }
+               }
+
+               /* Box blur */
+               int r = 5 * denoiser->params.radius;
+               float *data = buffer_data + 14;
+               array<float> temp(num_pixels);
+
+               for(int y = 0; y < h; y++) {
+                       for(int x = 0; x < w; x++) {
+                               int n = 0;
+                               float sum = 0.0f;
+                               for(int dx = max(x - r, 0); dx < min(x + r + 1, w); dx++, n++) {
+                                       sum += data[INPUT_NUM_CHANNELS * (y * w + dx)];
+                               }
+                               temp[y * w + x] = sum/n;
+                       }
+               }
+
+               for(int y = 0; y < h; y++) {
+                       for(int x = 0; x < w; x++) {
+                               int n = 0;
+                               float sum = 0.0f;
+
+                               for(int dy = max(y - r, 0); dy < min(y + r + 1, h); dy++, n++) {
+                                       sum += temp[dy * w + x];
+                               }
+
+                               data[INPUT_NUM_CHANNELS * (y * w + x)] = sum/n;
+                       }
+               }
+
+               buffer_data += frame_stride;
+       }
+
+       /* Copy to device */
+       input_pixels.copy_to_device();
+
+       return true;
+}
+
+/* Task stages */
+
+bool DenoiseTask::load()
+{
+       string center_filepath = denoiser->input[frame];
+       if(!image.load(center_filepath, error)) {
+               return false;
+       }
+
+       if(!image.load_neighbors(denoiser->input, neighbor_frames, error)) {
+               return false;
+       }
+
+       if(image.layers.empty()) {
+               error = "No image layers found to denoise in " + center_filepath;
+               return false;
+       }
+
+       /* Allocate device buffer. */
+       int num_frames = image.in_neighbors.size() + 1;
+       input_pixels.alloc(image.width * INPUT_NUM_CHANNELS, image.height * num_frames);
+       input_pixels.zero_to_device();
+
+       /* Read pixels for first layer. */
+       current_layer = 0;
+       if(!load_input_pixels(current_layer)) {
+               return false;
+       }
+
+       return true;
+}
+
+bool DenoiseTask::exec()
+{
+       for(current_layer = 0; current_layer < image.layers.size(); current_layer++) {
+               /* Read pixels for secondary layers, first was already loaded. */
+               if(current_layer > 0) {
+                       if(!load_input_pixels(current_layer)) {
+                               return false;
+                       }
+               }
+
+               /* Run task on device. */
+               DeviceTask task(DeviceTask::RENDER);
+               create_task(task);
+               device->task_add(task);
+               device->task_wait();
+
+               printf("\n");
+       }
+
+       return true;
+}
+
+bool DenoiseTask::save()
+{
+       bool ok = image.save_output(denoiser->output[frame], error);
+       free();
+       return ok;
+}
+
+void DenoiseTask::free()
+{
+       image.free();
+       input_pixels.free();
+       assert(output_pixels.empty());
+}
+
+/* Denoise Image Storage */
+
+DenoiseImage::DenoiseImage()
+{
+       in = NULL;
+
+       width = 0;
+       height = 0;
+       num_channels = 0;
+       samples = 0;
+}
+
+DenoiseImage::~DenoiseImage()
+{
+       free();
+}
+
+void DenoiseImage::close_input()
+{
+       foreach(ImageInput *i, in_neighbors) {
+               i->close();
+               ImageInput::destroy(i);
+       }
+
+       in_neighbors.clear();
+
+       if(in) {
+               in->close();
+               ImageInput::destroy(in);
+               in = NULL;
+       }
+}
+
+void DenoiseImage::free()
+{
+       close_input();
+       pixels.clear();
+}
+
+bool DenoiseImage::parse_channels(const ImageSpec &in_spec, string& error)
+{
+       const std::vector<string> &channels = in_spec.channelnames;
+       const ParamValue *multiview = in_spec.find_attribute("multiView");
+       const bool multiview_channels = (multiview &&
+                                        multiview->type().basetype == TypeDesc::STRING &&
+                                        multiview->type().arraylen >= 2);
+
+       layers.clear();
+
+       /* Loop over all the channels in the file, parse their name and sort them
+        * by RenderLayer.
+        * Channels that can't be parsed are directly passed through to the output. */
+       map<string, DenoiseImageLayer> file_layers;
+       for(int i = 0; i < channels.size(); i++) {
+               string layer, pass, channel;
+               if(parse_channel_name(channels[i], layer, pass, channel, multiview_channels)) {
+                       file_layers[layer].channels.push_back(pass + "." + channel);
+                       file_layers[layer].layer_to_image_channel.push_back(i);
+               }
+       }
+
+       /* Loop over all detected RenderLayers, check whether they contain a full set of input channels.
+        * Any channels that won't be processed internally are also passed through. */
+       for(map<string, DenoiseImageLayer>::iterator i = file_layers.begin(); i != file_layers.end(); ++i) {
+               const string& name = i->first;
+               DenoiseImageLayer& layer = i->second;
+
+               /* Check for full pass set. */
+               if(!layer.detect_denoising_channels()) {
+                       continue;
+               }
+
+               layer.name = name;
+               layer.samples = samples;
+
+               /* If the sample value isn't set yet, check if there is a layer-specific one in the input file. */
+               if(layer.samples < 1) {
+                       string sample_string = in_spec.get_string_attribute("cycles." + name + ".samples", "");
+                       if(sample_string != "") {
+                               if(!sscanf(sample_string.c_str(), "%d", &layer.samples)) {
+                                       error = "Failed to parse samples metadata: " + sample_string;
+                                       return false;
+                               }
+                       }
+               }
+
+               if(layer.samples < 1) {
+                       error = string_printf("No sample number specified in the file for layer %s or on the command line", name.c_str());
+                       return false;
+               }
+
+               layers.push_back(layer);
+       }
+
+       return true;
+}
+
+void DenoiseImage::read_pixels(const DenoiseImageLayer& layer, float *input_pixels)
+{
+       /* Pixels from center file have already been loaded into pixels.
+        * We copy a subset into the device input buffer with channels reshuffled. */
+       const int *input_to_image_channel = layer.input_to_image_channel.data();
+
+       for(int i = 0; i < width * height; i++) {
+               for(int j = 0; j < INPUT_NUM_CHANNELS; j++) {
+                       int image_channel = input_to_image_channel[j];
+                       input_pixels[i*INPUT_NUM_CHANNELS + j] = pixels[((size_t)i)*num_channels + image_channel];
+               }
+       }
+}
+
+bool DenoiseImage::read_neighbor_pixels(int neighbor, const DenoiseImageLayer& layer, float *input_pixels)
+{
+       /* Load pixels from neighboring frames, and copy them into device buffer
+        * with channels reshuffled. */
+       size_t num_pixels = (size_t)width * (size_t)height;
+       array<float> neighbor_pixels(num_pixels * num_channels);
+       if(!in_neighbors[neighbor]->read_image(TypeDesc::FLOAT, neighbor_pixels.data())) {
+               return false;
+       }
+
+       const int *input_to_image_channel = layer.neighbor_input_to_image_channel[neighbor].data();
+
+       for(int i = 0; i < width * height; i++) {
+               for(int j = 0; j < INPUT_NUM_CHANNELS; j++) {
+                       int image_channel = input_to_image_channel[j];
+                       input_pixels[i*INPUT_NUM_CHANNELS + j] = neighbor_pixels[((size_t)i)*num_channels + image_channel];
+               }
+       }
+
+       return true;
+}
+
+bool DenoiseImage::load(const string& in_filepath, string& error)
+{
+       if(!Filesystem::is_regular(in_filepath)) {
+               error = "Couldn't find file: " + in_filepath;
+               return false;
+       }
+
+       in = ImageInput::open(in_filepath);
+       if(!in) {
+               error = "Couldn't open file: " + in_filepath;
+               return false;
+       }
+
+       const ImageSpec &in_spec = in->spec();
+       width = in_spec.width;
+       height = in_spec.height;
+       num_channels = in_spec.nchannels;
+
+       if(!parse_channels(in_spec, error)) {
+               return false;
+       }
+
+       if(layers.size() == 0) {
+               error = "Could not find a render layer containing denoising info";
+               return false;
+       }
+
+       size_t num_pixels = (size_t)width * (size_t)height;
+       pixels.resize(num_pixels * num_channels);
+
+       /* Read all channels into buffer. Reading all channels at once is faster
+        * than individually due to interleaved EXR channel storage. */
+       if(!in->read_image(TypeDesc::FLOAT, pixels.data())) {
+               error = "Failed to read image: " + in_filepath;
+               return false;
+       }
+
+       return true;
+}
+
+bool DenoiseImage::load_neighbors(const vector<string>& filepaths, const vector<int>& frames, string& error)
+{
+       if(frames.size() > DENOISE_MAX_FRAMES - 1) {
+               error = string_printf("Maximum number of neighbors (%d) exceeded\n", DENOISE_MAX_FRAMES - 1);
+               return false;
+       }
+
+       for(int neighbor = 0; neighbor < frames.size(); neighbor++) {
+               int frame = frames[neighbor];
+               const string& filepath = filepaths[frame];
+
+               if(!Filesystem::is_regular(filepath)) {
+                       error = "Couldn't find neighbor frame: " + filepath;
+                       return false;
+               }
+
+               ImageInput *in_neighbor = ImageInput::open(filepath);
+               if(!in_neighbor) {
+                       error = "Couldn't open neighbor frame: " + filepath;
+                       return false;
+               }
+
+               const ImageSpec &neighbor_spec = in_neighbor->spec();
+               if(neighbor_spec.width != width || neighbor_spec.height != height) {
+                       error = "Neighbor frame has different dimensions: " + filepath;
+                       in_neighbor->close();
+                       ImageInput::destroy(in_neighbor);
+                       return false;
+               }
+
+               foreach(DenoiseImageLayer& layer, layers) {
+                       if(!layer.match_channels(neighbor,
+                                                in->spec().channelnames,
+                                                neighbor_spec.channelnames))
+                       {
+                               error = "Neighbor frame misses denoising data passes: " + filepath;
+                               in_neighbor->close();
+                               ImageInput::destroy(in_neighbor);
+                               return false;
+                       }
+               }
+
+               in_neighbors.push_back(in_neighbor);
+       }
+
+       return true;
+}
+
+bool DenoiseImage::save_output(const string& out_filepath, string& error)
+{
+       /* Save image with identical dimensions, channels and metadata. */
+       ImageSpec out_spec = in->spec();
+
+       /* Ensure that the output frame contains sample information even if the input didn't. */
+       for(int i = 0; i < layers.size(); i++) {
+               string name = "cycles." + layers[i].name + ".samples";
+               if(!out_spec.find_attribute(name, TypeDesc::STRING)) {
+                       out_spec.attribute(name, TypeDesc::STRING, string_printf("%d", layers[i].samples));
+               }
+       }
+
+       /* We don't need input anymore at this point, and will possibly
+        * overwrite the same file. */
+       close_input();
+
+       /* Write to temporary file path, so we denoise images in place and don't
+        * risk destroying files when something goes wrong in file saving. */
+       string tmp_filepath = OIIO::Filesystem::temp_directory_path() + "/" + OIIO::Filesystem::unique_path() + ".exr";
+       ImageOutput *out = ImageOutput::create(tmp_filepath);
+
+       if(!out) {
+               error = "Failed to open temporary file " + tmp_filepath + " for writing";
+               return false;
+       }
+
+       /* Open temporary file and write image buffers. */
+       if(!out->open(tmp_filepath, out_spec)) {
+               error = "Failed to open file " + tmp_filepath + " for writing: " + out->geterror();
+               ImageOutput::destroy(out);
+               return false;
+       }
+
+       bool ok = true;
+       if(!out->write_image(TypeDesc::FLOAT, pixels.data())) {
+               error = "Failed to write to file " + tmp_filepath + ": " + out->geterror();
+               ok = false;
+       }
+
+       if(!out->close()) {
+               error = "Failed to save to file " + tmp_filepath + ": " + out->geterror();
+               ok = false;
+       }
+
+       ImageOutput::destroy(out);
+
+       /* Copy temporary file to outputput filepath. */
+       if(ok && !OIIO::Filesystem::rename(tmp_filepath, out_filepath)) {
+               error = "Failed to save to file " + out_filepath + ": " + out->geterror();
+               ok = false;
+       }
+
+       if(!ok) {
+               OIIO::Filesystem::remove(tmp_filepath);
+               return false;
+       }
+
+       return true;
+}
+
+/* File pattern handling and outer loop over frames */
+
+Denoiser::Denoiser(DeviceInfo& device_info)
+{
+       samples_override = 0;
+       tile_size = make_int2(64, 64);
+
+       num_frames = 0;
+
+       /* Initialize task scheduler. */
+       TaskScheduler::init();
+
+       /* Initialize device. */
+       DeviceRequestedFeatures req;
+       device = Device::create(device_info, stats, profiler, true);
+       device->load_kernels(req);
+}
+
+Denoiser::~Denoiser()
+{
+       delete device;
+       TaskScheduler::exit();
+}
+
+bool Denoiser::run()
+{
+       assert(input.size() == output.size());
+
+       num_frames = output.size();
+
+       for(int frame = 0; frame < num_frames; frame++) {
+               /* Skip empty output paths. */
+               if(output[frame].empty()) {
+                       continue;
+               }
+
+               /* Determine neighbor frame numbers that should be used for filtering. */
+               vector<int> neighbor_frames;
+               for(int f = frame - params.neighbor_frames; f <= frame + params.neighbor_frames; f++) {
+                       if (f >= 0 && f < num_frames && f != frame) {
+                               neighbor_frames.push_back(f);
+                       }
+               }
+
+               /* Execute task. */
+               DenoiseTask task(device, this, frame, neighbor_frames);
+               if(!task.load()) {
+                       error = task.error;
+                       return false;
+               }
+
+               if(!task.exec()) {
+                       error = task.error;
+                       return false;
+               }
+
+               if(!task.save()) {
+                       error = task.error;
+                       return false;
+               }
+
+               task.free();
+       }
+
+       return true;
+}
+
+CCL_NAMESPACE_END
diff --git a/intern/cycles/render/denoising.h b/intern/cycles/render/denoising.h
new file mode 100644 (file)
index 0000000..15e690a
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2011-2018 Blender Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __DENOISING_H__
+#define __DENOISING_H__
+
+#include "device/device.h"
+#include "device/device_denoising.h"
+
+#include "render/buffers.h"
+
+#include "util/util_string.h"
+#include "util/util_vector.h"
+
+#include <OpenImageIO/imageio.h>
+
+OIIO_NAMESPACE_USING
+
+CCL_NAMESPACE_BEGIN
+
+/* Denoiser */
+
+class Denoiser {
+public:
+       Denoiser(DeviceInfo& device_info);
+       ~Denoiser();
+
+       bool run();
+
+       /* Error message after running, in case of failure. */
+       string error;
+
+       /* Sequential list of frame filepaths to denoise. */
+       vector<string> input;
+       /* Sequential list of frame filepaths to write result to. Empty entries
+        * are skipped, so only a subset of the sequence can be denoised while
+        * taking into account all input frames. */
+       vector<string> output;
+
+       /* Sample number override, takes precedence over values from input frames. */
+       int samples_override;
+       /* Tile size for processing on device. */
+       int2 tile_size;
+
+       /* Equivalent to the settings in the regular denoiser. */
+       DenoiseParams params;
+
+protected:
+       friend class DenoiseTask;
+
+       Stats stats;
+       Profiler profiler;
+       Device *device;
+
+       int num_frames;
+};
+
+/* Denoise Image Layer */
+
+struct DenoiseImageLayer {
+       string name;
+       /* All channels belonging to this DenoiseImageLayer. */
+       vector<string> channels;
+       /* Layer to image channel mapping. */
+       vector<int> layer_to_image_channel;
+
+       /* Sample amount that was used for rendering this layer. */
+       int samples;
+
+       /* Device input channel will be copied from image channel input_to_image_channel[i]. */
+       vector<int> input_to_image_channel;
+
+       /* input_to_image_channel of the secondary frames, if any are used. */
+       vector<vector<int> > neighbor_input_to_image_channel;
+
+       /* Write i-th channel of the processing output to output_to_image_channel[i]-th channel of the file. */
+       vector<int> output_to_image_channel;
+
+       /* Detect whether this layer contains a full set of channels and set up the offsets accordingly. */
+       bool detect_denoising_channels();
+
+       /* Map the channels of a secondary frame to the channels that are required for processing,
+        * fill neighbor_input_to_image_channel if all are present or return false if a channel are missing. */
+       bool match_channels(int neighbor,
+                           const std::vector<string> &channelnames,
+                           const std::vector<string> &neighbor_channelnames);
+};
+
+/* Denoise Image Data */
+
+class DenoiseImage {
+public:
+       DenoiseImage();
+       ~DenoiseImage();
+
+       /* Dimensions */
+       int width, height, num_channels;
+
+       /* Samples */
+       int samples;
+
+       /* Pixel buffer with interleaved channels. */
+       array<float> pixels;
+
+       /* Image file handles */
+       ImageInput *in;
+       vector<ImageInput*> in_neighbors;
+
+       /* Render layers */
+       vector<DenoiseImageLayer> layers;
+
+       void free();
+
+       /* Open the input image, parse its channels, open the output image and allocate the output buffer. */
+       bool load(const string& in_filepath, string& error);
+
+       /* Load neighboring frames. */
+       bool load_neighbors(const vector<string>& filepaths, const vector<int>& frames, string& error);
+
+       /* Load subset of pixels from file buffer into input buffer, as needed for denoising
+        * on the device. Channels are reshuffled following the provided mapping. */
+       void read_pixels(const DenoiseImageLayer& layer, float *input_pixels);
+       bool read_neighbor_pixels(int neighbor, const DenoiseImageLayer& layer, float *input_pixels);
+
+       bool save_output(const string& out_filepath, string& error);
+
+protected:
+       /* Parse input file channels, separate them into DenoiseImageLayers, detect DenoiseImageLayers with full channel sets,
+        * fill layers and set up the output channels and passthrough map. */
+       bool parse_channels(const ImageSpec &in_spec, string& error);
+
+       void close_input();
+};
+
+/* Denoise Task */
+
+class DenoiseTask {
+public:
+       DenoiseTask(Device *device, Denoiser *denoiser, int frame, const vector<int>& neighbor_frames);
+       ~DenoiseTask();
+
+       /* Task stages */
+       bool load();
+       bool exec();
+       bool save();
+       void free();
+
+       string error;
+
+protected:
+       /* Denoiser parameters and device */
+       Denoiser *denoiser;
+       Device *device;
+
+       /* Frame number to be denoised */
+       int frame;
+       vector<int> neighbor_frames;
+
+       /* Image file data */
+       DenoiseImage image;
+       int current_layer;
+
+       /* Device input buffer */
+       device_vector<float> input_pixels;
+
+       /* Tiles */
+       thread_mutex tiles_mutex;
+       list<RenderTile> tiles;
+       int num_tiles;
+
+       thread_mutex output_mutex;
+       map<int, device_vector<float>*> output_pixels;
+
+       /* Task handling */
+       bool load_input_pixels(int layer);
+       void create_task(DeviceTask& task);
+
+       /* Device task callbacks */
+       bool acquire_tile(Device *device, Device *tile_device, RenderTile &tile);
+       void map_neighboring_tiles(RenderTile *tiles, Device *tile_device);
+       void unmap_neighboring_tiles(RenderTile *tiles);
+       void release_tile();
+       bool get_cancel();
+};
+
+CCL_NAMESPACE_END
+
+#endif /* __DENOISING_H__ */
index abed5c1..ac69251 100644 (file)
@@ -979,10 +979,7 @@ void Session::render()
        task.passes_size = tile_manager.params.get_passes_size();
 
        if(params.run_denoising) {
-               task.denoising_radius = params.denoising_radius;
-               task.denoising_strength = params.denoising_strength;
-               task.denoising_feature_strength = params.denoising_feature_strength;
-               task.denoising_relative_pca = params.denoising_relative_pca;
+               task.denoising = params.denoising;
 
                assert(!scene->film->need_update);
                task.pass_stride = scene->film->pass_stride;
index cb1d8fe..e3cccbb 100644 (file)
@@ -63,10 +63,7 @@ public:
        bool run_denoising;
        bool write_denoising_passes;
        bool full_denoising;
-       int denoising_radius;
-       float denoising_strength;
-       float denoising_feature_strength;
-       bool denoising_relative_pca;
+       DenoiseParams denoising;
 
        double cancel_timeout;
        double reset_timeout;
@@ -98,10 +95,6 @@ public:
                run_denoising = false;
                write_denoising_passes = false;
                full_denoising = false;
-               denoising_radius = 8;
-               denoising_strength = 0.0f;
-               denoising_feature_strength = 0.0f;
-               denoising_relative_pca = false;
 
                display_buffer_linear = false;
 
index a22bd25..6255596 100644 (file)
@@ -32,6 +32,7 @@
 #  include <sys/types.h>
 #else
 #  include <unistd.h>
+#  include <sys/ioctl.h>
 #endif
 
 CCL_NAMESPACE_BEGIN
@@ -113,6 +114,25 @@ bool system_cpu_run_thread_on_node(int node)
        return numaAPI_RunThreadOnNode(node);
 }
 
+int system_console_width()
+{
+       int columns = 0;
+
+#ifdef _WIN32
+       CONSOLE_SCREEN_BUFFER_INFO csbi;
+       if(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
+               columns = csbi.dwSize.X;
+       }
+#else
+       struct winsize w;
+       if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
+               columns = w.ws_col;
+       }
+#endif
+
+       return (columns > 0) ? columns : 80;
+}
+
 int system_cpu_num_active_group_processors()
 {
        if(!system_cpu_ensure_initialized()) {
index 0c001f1..1e7cf1d 100644 (file)
@@ -27,6 +27,9 @@ bool system_cpu_ensure_initialized();
 /* Get total number of threads in all NUMA nodes / CPU groups. */
 int system_cpu_thread_count();
 
+/* Get width in characters of the current console output. */
+int system_console_width();
+
 /* Get number of available nodes.
  *
  * This is in fact an index of last node plus one and it's not guaranteed