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:39:08 +0000 (13:39 +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!

Differential Revision: https://developer.blender.org/D3889

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 769bc788ab5d99352ac18540466d043a18a3bc51..84e2690333e1115b915a78804f23dafd823ab465 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 038126278aa5d9a8bee52a8483b3ab63ab080054..1f148538328c6c1d8aa0f97579c3cdd28d588c0f 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 23ab1cf6a30dfea417b2d966e15d98e370e334a2..1106923f529b95bbe6993667ddaf210f0cb1f6d9 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 2f1adfe41786437d319163d12d726a4e28ab0872..e372843d7638c16bed4accbbf8b1923278abcbb5 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 9dd0cd4c0bc6d78f8242a400cdbd12187cda5b25..bf7605ed5b141754068322e1aae25102512f5210 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 50ac35069a9775c5380216861716bb64723a95d7..43d7ff49a3bfae1f42c19103a7db8b8ad1a20a1f 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 61e0ba47ab8e0c391ed675491db3964a1d95b552..1bb144ef85ac339d05ac7657632643237c3c7323 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 2871bc5761a894bc140fb1fb0bee795c59e98714..f1fd42468689efa2b0bef6f4e111cbf076f77d61 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 c0ce7368771a79b1ad0922b7c9970b2d40c4ca10..e6afbc50463d19e68718489ecca78aca71516958 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 abed5c13f9b777d7146b1ff851b5ce2de4782884..ac69251c9080bd5cc6ce5e10e7f02cd7b4cd554e 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 cb1d8fed68faa951aebd183ed0a264c494eef911..e3cccbb9fcf95f3e46c45f4c2532d037aee78f39 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 a22bd25ce77d79c99ae155c4c0500fec1b51799f..6255596cd06764f15bd60780b9218a183c3aeffe 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 0c001f11f0e97f607af11e36a10f12a04a102291..1e7cf1d9f2a00648045bad6693b5c4379e46fedf 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