Merge branch 'blender2.7'
authorBrecht Van Lommel <brechtvanlommel@gmail.com>
Tue, 19 Mar 2019 17:54:17 +0000 (18:54 +0100)
committerBrecht Van Lommel <brechtvanlommel@gmail.com>
Tue, 19 Mar 2019 17:54:17 +0000 (18:54 +0100)
intern/cycles/blender/addon/operators.py
intern/cycles/blender/blender_python.cpp
intern/cycles/blender/blender_session.cpp
intern/cycles/blender/blender_util.h
intern/cycles/render/CMakeLists.txt
intern/cycles/render/merge.cpp [new file with mode: 0644]
intern/cycles/render/merge.h [new file with mode: 0644]
intern/cycles/test/CMakeLists.txt
intern/cycles/test/util_time_test.cpp [new file with mode: 0644]
intern/cycles/util/util_time.cpp
intern/cycles/util/util_time.h

index 63c9868ca6ac209553c248756a09c1c10537425d..b53679bd3284d51718ef98e07ef4c923f4021bc5 100644 (file)
@@ -124,9 +124,48 @@ class CYCLES_OT_denoise_animation(Operator):
         return {'FINISHED'}
 
 
+class CYCLES_OT_merge_images(Operator):
+    "Combine OpenEXR multilayer images rendered with different sample" \
+    "ranges into one image with reduced noise."
+    bl_idname = "cycles.merge_images"
+    bl_label = "Merge Images"
+
+    input_filepath1: StringProperty(
+        name='Input Filepath',
+        description='File path for image to merge',
+        default='',
+        subtype='FILE_PATH')
+
+    input_filepath2: StringProperty(
+        name='Input Filepath',
+        description='File path for image to merge',
+        default='',
+        subtype='FILE_PATH')
+
+    output_filepath: StringProperty(
+        name='Output Filepath',
+        description='File path for merged image',
+        default='',
+        subtype='FILE_PATH')
+
+    def execute(self, context):
+        in_filepaths = [self.input_filepath1, self.input_filepath2]
+        out_filepath = self.output_filepath
+
+        import _cycles
+        try:
+            _cycles.merge(input=in_filepaths, output=out_filepath)
+        except Exception as e:
+            self.report({'ERROR'}, str(e))
+            return {'FINISHED'}
+
+        return {'FINISHED'}
+
+
 classes = (
     CYCLES_OT_use_shading_nodes,
-    CYCLES_OT_denoise_animation
+    CYCLES_OT_denoise_animation,
+    CYCLES_OT_merge_images
 )
 
 def register():
index 4a1eeeb65c1789a38edcb6c8847a1c91b380e8ca..ccd783073a3596a824844ff116e59cbc00aea094 100644 (file)
@@ -23,6 +23,7 @@
 #include "blender/blender_session.h"
 
 #include "render/denoising.h"
+#include "render/merge.h"
 
 #include "util/util_debug.h"
 #include "util/util_foreach.h"
@@ -642,9 +643,8 @@ static PyObject *opencl_compile_func(PyObject * /*self*/, PyObject *args)
 }
 #endif
 
-static bool denoise_parse_filepaths(PyObject *pyfilepaths, vector<string>& filepaths)
+static bool image_parse_filepaths(PyObject *pyfilepaths, vector<string>& filepaths)
 {
-
        if(PyUnicode_Check(pyfilepaths)) {
                const char *filepath = PyUnicode_AsUTF8(pyfilepaths);
                filepaths.push_back(filepath);
@@ -713,12 +713,12 @@ static PyObject *denoise_func(PyObject * /*self*/, PyObject *args, PyObject *key
        /* Parse file paths list. */
        vector<string> input, output;
 
-       if(!denoise_parse_filepaths(pyinput, input)) {
+       if(!image_parse_filepaths(pyinput, input)) {
                return NULL;
        }
 
        if(pyoutput) {
-               if(!denoise_parse_filepaths(pyoutput, output)) {
+               if(!image_parse_filepaths(pyoutput, output)) {
                        return NULL;
                }
        }
@@ -757,6 +757,42 @@ static PyObject *denoise_func(PyObject * /*self*/, PyObject *args, PyObject *key
        Py_RETURN_NONE;
 }
 
+static PyObject *merge_func(PyObject * /*self*/, PyObject *args, PyObject *keywords)
+{
+       static const char *keyword_list[] = {"input", "output", NULL};
+       PyObject *pyinput, *pyoutput = NULL;
+
+       if (!PyArg_ParseTupleAndKeywords(args, keywords, "OO", (char**)keyword_list, &pyinput, &pyoutput)) {
+               return NULL;
+       }
+
+       /* Parse input list. */
+       vector<string> input;
+       if(!image_parse_filepaths(pyinput, input)) {
+               return NULL;
+       }
+
+       /* Parse output string. */
+       if(!PyUnicode_Check(pyoutput)) {
+               PyErr_SetString(PyExc_ValueError, "Output must be a string.");
+               return NULL;
+       }
+       string output = PyUnicode_AsUTF8(pyoutput);
+
+       /* Merge. */
+       ImageMerger merger;
+       merger.input = input;
+       merger.output = output;
+
+       if(!merger.run()) {
+               PyErr_SetString(PyExc_ValueError, merger.error.c_str());
+               return NULL;
+       }
+
+       Py_RETURN_NONE;
+}
+
+
 static PyObject *debug_flags_update_func(PyObject * /*self*/, PyObject *args)
 {
        PyObject *pyscene;
@@ -920,6 +956,7 @@ static PyMethodDef methods[] = {
 
        /* Standalone denoising */
        {"denoise", (PyCFunction)denoise_func, METH_VARARGS|METH_KEYWORDS, ""},
+       {"merge", (PyCFunction)merge_func, METH_VARARGS|METH_KEYWORDS, ""},
 
        /* Debugging routines */
        {"debug_flags_update", debug_flags_update_func, METH_VARARGS, ""},
index a06135b5362377b219d3715a9e9caa698eb128d2..cf856c3b3d4e677b1687992d2dc961a2106923dd 100644 (file)
@@ -393,15 +393,6 @@ static void add_cryptomatte_layer(BL::RenderResult& b_rr, string name, string ma
        render_add_metadata(b_rr, prefix+"manifest", manifest);
 }
 
-/* TODO(sergey): Ideally this will be an utility function in util string.h, but
- * currently is relying on Blender side function, so can not do that. */
-static string make_human_readable_time(double time)
-{
-       char time_str[128];
-       BLI_timecode_string_from_time_simple(time_str, sizeof(time_str), time);
-       return time_str;
-}
-
 void BlenderSession::stamp_view_layer_metadata(Scene *scene, const string& view_layer_name)
 {
        BL::RenderResult b_rr = b_engine.get_result();
@@ -440,11 +431,11 @@ void BlenderSession::stamp_view_layer_metadata(Scene *scene, const string& view_
        double total_time, render_time;
        session->progress.get_time(total_time, render_time);
        b_rr.stamp_data_add_field((prefix + "total_time").c_str(),
-                                 make_human_readable_time(total_time).c_str());
+                                 time_human_readable_from_seconds(total_time).c_str());
        b_rr.stamp_data_add_field((prefix + "render_time").c_str(),
-                                 make_human_readable_time(render_time).c_str());
+                                 time_human_readable_from_seconds(render_time).c_str());
        b_rr.stamp_data_add_field((prefix + "synchronization_time").c_str(),
-                                 make_human_readable_time(total_time - render_time).c_str());
+                                 time_human_readable_from_seconds(total_time - render_time).c_str());
 }
 
 void BlenderSession::render(BL::Depsgraph& b_depsgraph_)
@@ -1014,7 +1005,6 @@ void BlenderSession::update_status_progress()
        string scene_status = "";
        float progress;
        double total_time, remaining_time = 0, render_time;
-       char time_str[128];
        float mem_used = (float)session->stats.mem_used / 1024.0f / 1024.0f;
        float mem_peak = (float)session->stats.mem_peak / 1024.0f / 1024.0f;
 
@@ -1034,8 +1024,7 @@ void BlenderSession::update_status_progress()
                        scene_status += ", " + b_rview_name;
 
                if(remaining_time > 0) {
-                       BLI_timecode_string_from_time_simple(time_str, sizeof(time_str), remaining_time);
-                       timestatus += "Remaining:" + string(time_str) + " | ";
+                       timestatus += "Remaining:" + time_human_readable_from_seconds(remaining_time) + " | ";
                }
 
                timestatus += string_printf("Mem:%.2fM, Peak:%.2fM", (double)mem_used, (double)mem_peak);
index b9a1de087056603f8e104f90922e6d9ea3d12e11..ec836bd5ec1e68b50545b6e963728211e0cabf68 100644 (file)
@@ -32,7 +32,6 @@
  * todo: clean this up ... */
 
 extern "C" {
-size_t BLI_timecode_string_from_time_simple(char *str, size_t maxlen, double time_seconds);
 void BKE_image_user_frame_calc(void *iuser, int cfra);
 void BKE_image_user_file_path(void *iuser, void *ima, char *path);
 unsigned char *BKE_image_get_pixels_for_frame(void *image, int frame);
index e6afbc50463d19e68718489ecca78aca71516958..b7c53f17c3d88e2bf56d2ced9b56ad5ce7934d65 100644 (file)
@@ -22,6 +22,7 @@ set(SRC
        image.cpp
        integrator.cpp
        light.cpp
+       merge.cpp
        mesh.cpp
        mesh_displace.cpp
        mesh_subdivision.cpp
@@ -55,6 +56,7 @@ set(SRC_HEADERS
        image.h
        integrator.h
        light.h
+       merge.h
        mesh.h
        nodes.h
        object.h
diff --git a/intern/cycles/render/merge.cpp b/intern/cycles/render/merge.cpp
new file mode 100644 (file)
index 0000000..af65541
--- /dev/null
@@ -0,0 +1,456 @@
+/*
+ * 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.
+ */
+
+#include "render/merge.h"
+
+#include "util/util_array.h"
+#include "util/util_map.h"
+#include "util/util_system.h"
+#include "util/util_time.h"
+#include "util/util_unique_ptr.h"
+
+#include <OpenImageIO/imageio.h>
+#include <OpenImageIO/filesystem.h>
+
+OIIO_NAMESPACE_USING
+
+CCL_NAMESPACE_BEGIN
+
+/* Merge Image Layer */
+
+enum MergeChannelOp {
+       MERGE_CHANNEL_COPY,
+       MERGE_CHANNEL_SUM,
+       MERGE_CHANNEL_AVERAGE
+};
+
+struct MergeImageLayer {
+       /* Layer name. */
+       string name;
+
+       /* All channels belonging to this MergeImageLayer. */
+       vector<string> channel_names;
+       /* Offsets of layer channels in image. */
+       vector<int> channel_offsets;
+       /* Type of operation to perform when merging. */
+       vector<MergeChannelOp> channel_ops;
+
+       /* Sample amount that was used for rendering this layer. */
+       int samples;
+};
+
+/* Merge Image */
+
+class MergeImage {
+public:
+       /* OIIO file handle. */
+       unique_ptr<ImageInput> in;
+
+       /* Image file path. */
+       string filepath;
+
+       /* Render layers. */
+       vector<MergeImageLayer> layers;
+};
+
+/* Channel Parsing */
+
+static MergeChannelOp parse_channel_operation(const string& pass_name)
+{
+       if(pass_name == "Depth" ||
+          pass_name == "IndexMA" ||
+          pass_name == "IndexOB" ||
+          string_startswith(pass_name, "Crypto"))
+       {
+               return MERGE_CHANNEL_COPY;
+       }
+       else if(string_startswith(pass_name, "Debug BVH") ||
+               string_startswith(pass_name, "Debug Ray") ||
+               string_startswith(pass_name, "Debug Render Time"))
+       {
+               return MERGE_CHANNEL_SUM;
+       }
+       else {
+               return MERGE_CHANNEL_AVERAGE;
+       }
+}
+
+/* Splits in at its last dot, setting suffix to the part after the dot and
+ * into 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.
+ * Multiview format: RenderLayer.Pass.View.Channel
+ * Otherwise: 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;
+}
+
+static bool parse_channels(const ImageSpec &in_spec,
+                           vector<MergeImageLayer>& layers,
+                           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, MergeImageLayer> 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].channel_names.push_back(pass + "." + channel);
+                       file_layers[layer].channel_offsets.push_back(i);
+                       file_layers[layer].channel_ops.push_back(parse_channel_operation(pass));
+               }
+
+               /* Any unparsed channels are copied from the first image. */
+       }
+
+       /* 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, MergeImageLayer>::iterator i = file_layers.begin(); i != file_layers.end(); ++i) {
+               const string& name = i->first;
+               MergeImageLayer& layer = i->second;
+
+               layer.name = name;
+               layer.samples = 0;
+
+               /* 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;
+}
+
+static bool open_images(const vector<string>& filepaths,
+                        vector<MergeImage>& images,
+                        string& error)
+{
+       for(const string& filepath: filepaths) {
+               unique_ptr<ImageInput> in(ImageInput::open(filepath));
+               if(!in) {
+                       error = "Couldn't open file: " + filepath;
+                       return false;
+               }
+
+               MergeImage image;
+               image.in = std::move(in);
+               image.filepath = filepath;
+               if(!parse_channels(image.in->spec(), image.layers, error)) {
+                       return false;
+               }
+
+               if(image.layers.size() == 0) {
+                       error = "Could not find a render layer for merging";
+                       return false;
+               }
+
+               if(image.in->spec().deep) {
+                       error = "Merging deep images not supported.";
+                       return false;
+               }
+
+               if(images.size() > 0) {
+                       const ImageSpec& base_spec = images[0].in->spec();
+                       const ImageSpec& spec = image.in->spec();
+
+                       if(base_spec.width != spec.width ||
+                          base_spec.height != spec.height ||
+                          base_spec.depth != spec.depth ||
+                          base_spec.nchannels != spec.nchannels ||
+                          base_spec.format != spec.format ||
+                          base_spec.channelformats != spec.channelformats ||
+                          base_spec.channelnames != spec.channelnames ||
+                          base_spec.deep != spec.deep)
+                       {
+                               error = "Images do not have exact matching data and channel layout.";
+                               return false;
+                       }
+               }
+
+               images.push_back(std::move(image));
+       }
+
+       return true;
+}
+
+static bool load_pixels(const MergeImage& image, array<float>& pixels, string& error)
+{
+       const ImageSpec& in_spec = image.in->spec();
+       const size_t width = in_spec.width;
+       const size_t height = in_spec.height;
+       const size_t num_channels = in_spec.nchannels;
+
+       const 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(!image.in->read_image(TypeDesc::FLOAT, pixels.data())) {
+               error = "Failed to read image: " + image.filepath;
+               return false;
+       }
+
+       return true;
+}
+
+static void merge_render_time(ImageSpec& spec,
+                              const vector<MergeImage>& images,
+                              const string& name,
+                              const bool average)
+{
+       double time = 0.0;
+
+       for(const MergeImage& image: images) {
+               string time_str = image.in->spec().get_string_attribute(name, "");
+               time += time_human_readable_to_seconds(time_str);
+       }
+
+       if(average) {
+               time /= images.size();
+       }
+
+       spec.attribute(name, TypeDesc::STRING, time_human_readable_from_seconds(time));
+}
+
+static void merge_layer_render_time(ImageSpec& spec,
+                                    const vector<MergeImage>& images,
+                                    const string& time_name,
+                                    const bool average)
+{
+       for(size_t i = 0; i < images[0].layers.size(); i++) {
+               string name = "cycles." + images[0].layers[i].name + "." + time_name;
+               double time = 0.0;
+
+               for(const MergeImage& image: images) {
+                       string time_str = image.in->spec().get_string_attribute(name, "");
+                       time += time_human_readable_to_seconds(time_str);
+               }
+
+               if(average) {
+                       time /= images.size();
+               }
+
+               spec.attribute(name, TypeDesc::STRING, time_human_readable_from_seconds(time));
+       }
+}
+
+static bool save_output(const string& filepath,
+                        const ImageSpec& spec,
+                        const array<float>& pixels,
+                        string& error)
+{
+       /* Write to temporary file path, so we merge images in place and don't
+        * risk destroying files when something goes wrong in file saving. */
+       string extension = OIIO::Filesystem::extension(filepath);
+       string unique_name = ".merge-tmp-" + OIIO::Filesystem::unique_path();
+       string tmp_filepath = filepath + unique_name + extension;
+       unique_ptr<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, spec)) {
+               error = "Failed to open file " + tmp_filepath + " for writing: " + out->geterror();
+               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;
+       }
+
+       out.reset();
+
+       /* Copy temporary file to outputput filepath. */
+       string rename_error;
+       if(ok && !OIIO::Filesystem::rename(tmp_filepath, filepath, rename_error)) {
+               error = "Failed to move merged image to " + filepath + ": " + rename_error;
+               ok = false;
+       }
+
+       if(!ok) {
+               OIIO::Filesystem::remove(tmp_filepath);
+       }
+
+       return ok;
+}
+
+/* Image Merger */
+
+ImageMerger::ImageMerger()
+{
+}
+
+bool ImageMerger::run()
+{
+       if(input.empty()) {
+               error = "No input file paths specified.";
+               return false;
+       }
+       if(output.empty()) {
+               error = "No output file path specified.";
+               return false;
+       }
+
+       /* Open images and verify they have matching layout. */
+       vector<MergeImage> images;
+       if(!open_images(input, images, error)) {
+               return false;
+       }
+
+       /* Merge pixels. */
+       array<float> merge_pixels;
+       vector<int> merge_samples;
+
+       /* Load first image. */
+       if(!load_pixels(images[0], merge_pixels, error)) {
+               return false;
+       }
+       for(size_t layer = 0; layer < images[0].layers.size(); layer++) {
+               merge_samples.push_back(images[0].layers[layer].samples);
+       }
+
+       /* Merge other images. */
+       for(size_t i = 1; i < images.size(); i++) {
+               const MergeImage& image = images[i];
+
+               array<float> pixels;
+               if(!load_pixels(image, pixels, error)) {
+                       return false;
+               }
+
+               for(size_t li = 0; li < image.layers.size(); li++) {
+                       const MergeImageLayer& layer = image.layers[li];
+
+                       const int *offsets = layer.channel_offsets.data();
+                       const MergeChannelOp *ops = layer.channel_ops.data();
+
+                       const size_t stride = image.in->spec().nchannels;
+                       const size_t num_channels = layer.channel_offsets.size();
+                       const size_t num_pixels = pixels.size();
+
+                       /* Weights based on sample metadata. */
+                       const int sum_samples = merge_samples[li] + layer.samples;
+                       const float t = (float)layer.samples / (float)sum_samples;
+
+                       for(size_t pixel = 0; pixel < num_pixels; pixel += stride) {
+                               for(size_t channel = 0; channel < num_channels; channel++) {
+                                       size_t offset = pixel + offsets[channel];
+
+                                       switch(ops[channel]) {
+                                               case MERGE_CHANNEL_COPY:
+                                                       /* Already copied from first image. */
+                                                       break;
+                                               case MERGE_CHANNEL_SUM:
+                                                       merge_pixels[offset] += pixels[offset];
+                                                       break;
+                                               case MERGE_CHANNEL_AVERAGE:
+                                                       merge_pixels[offset] = (1.0f - t) * merge_pixels[offset] + t * pixels[offset];
+                                                       break;
+                                       }
+                               }
+                       }
+
+                       merge_samples[li] += layer.samples;
+               }
+       }
+
+       /* Save image with identical dimensions, channels and metadata. */
+       ImageSpec out_spec = images[0].in->spec();
+
+       /* Merge metadata. */
+       for(size_t i = 0; i < images[0].layers.size(); i++) {
+               string name = "cycles." + images[0].layers[i].name + ".samples";
+               out_spec.attribute(name, TypeDesc::STRING, string_printf("%d", merge_samples[i]));
+       }
+
+       merge_render_time(out_spec, images, "RenderTime", false);
+       merge_layer_render_time(out_spec, images, "total_time", false);
+       merge_layer_render_time(out_spec, images, "render_time", false);
+       merge_layer_render_time(out_spec, images, "synchronization_time", true);
+
+       /* We don't need input anymore at this point, and will possibly
+        * overwrite the same file. */
+       images.clear();
+
+       /* Save output file. */
+       return save_output(output, out_spec, merge_pixels, error);
+}
+
+CCL_NAMESPACE_END
diff --git a/intern/cycles/render/merge.h b/intern/cycles/render/merge.h
new file mode 100644 (file)
index 0000000..48900a1
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+#ifndef __MERGE_H__
+#define __MERGE_H__
+
+#include "util/util_string.h"
+#include "util/util_vector.h"
+
+CCL_NAMESPACE_BEGIN
+
+/* Merge OpenEXR multilayer renders. */
+
+class ImageMerger {
+public:
+       ImageMerger();
+       bool run();
+
+       /* Error message after running, in case of failure. */
+       string error;
+
+       /* List of image filepaths to merge. */
+       vector<string> input;
+       /* Output filepath. */
+       string output;
+};
+
+CCL_NAMESPACE_END
+
+#endif /* __MERGE_H__ */
index 7e7e9af76e54998c020ca66147fff84d39232670..73fe590f8ae1c300363746b8b739944c98982f06 100644 (file)
@@ -103,3 +103,4 @@ CYCLES_TEST(util_aligned_malloc "cycles_util")
 CYCLES_TEST(util_path "cycles_util;${BOOST_LIBRARIES};${OPENIMAGEIO_LIBRARIES}")
 CYCLES_TEST(util_string "cycles_util;${BOOST_LIBRARIES};${OPENIMAGEIO_LIBRARIES}")
 CYCLES_TEST(util_task "cycles_util;${BOOST_LIBRARIES};${OPENIMAGEIO_LIBRARIES};bf_intern_numaapi")
+CYCLES_TEST(util_time "cycles_util;${BOOST_LIBRARIES};${OPENIMAGEIO_LIBRARIES}")
diff --git a/intern/cycles/test/util_time_test.cpp b/intern/cycles/test/util_time_test.cpp
new file mode 100644 (file)
index 0000000..74f9f3b
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+#include "testing/testing.h"
+
+#include "util/util_time.h"
+
+CCL_NAMESPACE_BEGIN
+
+TEST(time_human_readable_to_seconds, Empty) {
+       EXPECT_EQ(time_human_readable_to_seconds(""), 0.0);
+       EXPECT_EQ(time_human_readable_from_seconds(0.0), "00:00.00");
+}
+
+TEST(time_human_readable_to_seconds, Fraction) {
+       EXPECT_NEAR(time_human_readable_to_seconds(".1"), 0.1, 1e-8f);
+       EXPECT_NEAR(time_human_readable_to_seconds(".10"), 0.1, 1e-8f);
+       EXPECT_EQ(time_human_readable_from_seconds(0.1), "00:00.10");
+}
+
+TEST(time_human_readable_to_seconds, Seconds) {
+       EXPECT_NEAR(time_human_readable_to_seconds("2.1"), 2.1, 1e-8f);
+       EXPECT_NEAR(time_human_readable_to_seconds("02.10"), 2.1, 1e-8f);
+       EXPECT_EQ(time_human_readable_from_seconds(2.1), "00:02.10");
+
+       EXPECT_NEAR(time_human_readable_to_seconds("12.1"), 12.1, 1e-8f);
+       EXPECT_NEAR(time_human_readable_to_seconds("12.10"), 12.1, 1e-8f);
+       EXPECT_EQ(time_human_readable_from_seconds(12.1), "00:12.10");
+}
+
+TEST(time_human_readable_to_seconds, MinutesSeconds) {
+       EXPECT_NEAR(time_human_readable_to_seconds("3:2.1"), 182.1, 1e-8f);
+       EXPECT_NEAR(time_human_readable_to_seconds("03:02.10"), 182.1, 1e-8f);
+       EXPECT_EQ(time_human_readable_from_seconds(182.1), "03:02.10");
+
+       EXPECT_NEAR(time_human_readable_to_seconds("34:12.1"), 2052.1, 1e-8f);
+       EXPECT_NEAR(time_human_readable_to_seconds("34:12.10"), 2052.1, 1e-8f);
+       EXPECT_EQ(time_human_readable_from_seconds(2052.1), "34:12.10");
+}
+
+TEST(time_human_readable_to_seconds, HoursMinutesSeconds) {
+       EXPECT_NEAR(time_human_readable_to_seconds("4:3:2.1"), 14582.1, 1e-8f);
+       EXPECT_NEAR(time_human_readable_to_seconds("04:03:02.10"), 14582.1, 1e-8f);
+       EXPECT_EQ(time_human_readable_from_seconds(14582.1), "04:03:02.10");
+
+       EXPECT_NEAR(time_human_readable_to_seconds("56:34:12.1"), 203652.1, 1e-8f);
+       EXPECT_NEAR(time_human_readable_to_seconds("56:34:12.10"), 203652.1, 1e-8f);
+       EXPECT_EQ(time_human_readable_from_seconds(203652.1), "56:34:12.10");
+}
+
+CCL_NAMESPACE_END
index bc6ac4e2099670209d3bbf9ac1357940ef677a79..9983fdd1df39390b0541f6b7fe7c4afe6e0d569c 100644 (file)
  * limitations under the License.
  */
 
+#include "util/util_time.h"
+
 #include <stdlib.h>
 
-#include "util/util_time.h"
-#include "util/util_windows.h"
+#if !defined(_WIN32)
+#  include <sys/time.h>
+#  include <unistd.h>
+#endif
 
-#ifdef _WIN32
+#include "util/util_math.h"
+#include "util/util_string.h"
+#include "util/util_windows.h"
 
 CCL_NAMESPACE_BEGIN
 
+#ifdef _WIN32
 double time_dt()
 {
        __int64 frequency, counter;
@@ -37,16 +44,7 @@ void time_sleep(double t)
 {
        Sleep((int)(t*1000));
 }
-
-CCL_NAMESPACE_END
-
 #else
-
-#include <sys/time.h>
-#include <unistd.h>
-
-CCL_NAMESPACE_BEGIN
-
 double time_dt()
 {
        struct timeval now;
@@ -73,7 +71,69 @@ void time_sleep(double t)
        if(us > 0)
                usleep(us);
 }
+#endif
 
-CCL_NAMESPACE_END
+/* Time in format "hours:minutes:seconds.hundreds" */
 
-#endif
+string time_human_readable_from_seconds(const double seconds)
+{
+       const int h = (((int)seconds) / (60 * 60));
+       const int m = (((int)seconds) / 60) % 60;
+       const int s = (((int)seconds) % 60);
+       const int r = (((int)(seconds * 100)) % 100);
+
+       if(h > 0) {
+               return string_printf("%.2d:%.2d:%.2d.%.2d", h, m, s, r);
+       }
+       else {
+               return string_printf("%.2d:%.2d.%.2d", m, s, r);
+       }
+}
+
+double time_human_readable_to_seconds(const string& time_string)
+{
+       /* Those are multiplies of a corresponding token surrounded by : in the
+        * time string, which denotes how to convert value to seconds.
+        * Effectively: seconds, minutes, hours, days in seconds. */
+       const int multipliers[] = {1, 60, 60*60, 24*60*60};
+       const int num_multiplies = sizeof(multipliers) / sizeof(*multipliers);
+       if(time_string.empty()) {
+               return 0.0;
+       }
+       double result = 0.0;
+       /* Split fractions of a second from the encoded time. */
+       vector<string> fraction_tokens;
+       string_split(fraction_tokens, time_string, ".", false);
+       const int num_fraction_tokens = fraction_tokens.size();
+       if(num_fraction_tokens == 0) {
+               /* Time string is malformed. */
+               return 0.0;
+       }
+       else if(fraction_tokens.size() == 1) {
+               /* There is no fraction of a second specified, the rest of the code
+                * handles this normally. */
+       }
+       else if(fraction_tokens.size() == 2) {
+               result = atof(fraction_tokens[1].c_str());
+               result *= pow(0.1, fraction_tokens[1].length());
+       }
+       else {
+               /* This is not a valid string, the result can not be reliable. */
+               return 0.0;
+       }
+       /* Split hours, minutes and seconds.
+        * Hours part is optional. */
+       vector<string> tokens;
+       string_split(tokens, fraction_tokens[0], ":", false);
+       const int num_tokens = tokens.size();
+       if(num_tokens > num_multiplies) {
+               /* Can not reliably represent the value. */
+               return 0.0;
+       }
+       for(int i = 0; i < num_tokens; ++i) {
+               result += atoi(tokens[num_tokens - i - 1].c_str()) * multipliers[i];
+       }
+       return result;
+}
+
+CCL_NAMESPACE_END
index 13281bf188b8009afb2d9e56a7055ae09285b805..ed4dd5154d7bdfa6a48285a59f794abbc354fd29 100644 (file)
 #ifndef __UTIL_TIME_H__
 #define __UTIL_TIME_H__
 
+#include "util/util_string.h"
+
 CCL_NAMESPACE_BEGIN
 
 /* Give current time in seconds in double precision, with good accuracy. */
 
 double time_dt();
 
-/* Sleep for the specified number of seconds */
+/* Sleep for the specified number of seconds. */
 
 void time_sleep(double t);
 
+/* Scoped timer. */
+
 class scoped_timer {
 public:
        explicit scoped_timer(double *value = NULL) : value_(value)
@@ -56,6 +60,11 @@ protected:
        double time_start_;
 };
 
+/* Make human readable string from time, compatible with Blender metadata. */
+
+string time_human_readable_from_seconds(const double seconds);
+double time_human_readable_to_seconds(const string& str);
+
 CCL_NAMESPACE_END
 
 #endif