diff options
author | Brecht Van Lommel <brechtvanlommel@gmail.com> | 2019-03-19 20:54:17 +0300 |
---|---|---|
committer | Brecht Van Lommel <brechtvanlommel@gmail.com> | 2019-03-19 20:54:17 +0300 |
commit | fa59c6a3e7d72d57faadcf0795848b4c3b8381d9 (patch) | |
tree | ea668f42028f8cdf5a5968feeed27e8bd99b30fd /intern | |
parent | e7f535cd4cf919b7e479d20b8a1b6714f166151d (diff) | |
parent | 83de13f75aafca7d4e1d58ddd36cca19121aa84f (diff) |
Merge branch 'blender2.7'
Diffstat (limited to 'intern')
-rw-r--r-- | intern/cycles/blender/addon/operators.py | 41 | ||||
-rw-r--r-- | intern/cycles/blender/blender_python.cpp | 45 | ||||
-rw-r--r-- | intern/cycles/blender/blender_session.cpp | 19 | ||||
-rw-r--r-- | intern/cycles/blender/blender_util.h | 1 | ||||
-rw-r--r-- | intern/cycles/render/CMakeLists.txt | 2 | ||||
-rw-r--r-- | intern/cycles/render/merge.cpp | 456 | ||||
-rw-r--r-- | intern/cycles/render/merge.h | 43 | ||||
-rw-r--r-- | intern/cycles/test/CMakeLists.txt | 1 | ||||
-rw-r--r-- | intern/cycles/test/util_time_test.cpp | 64 | ||||
-rw-r--r-- | intern/cycles/util/util_time.cpp | 88 | ||||
-rw-r--r-- | intern/cycles/util/util_time.h | 11 |
11 files changed, 735 insertions, 36 deletions
diff --git a/intern/cycles/blender/addon/operators.py b/intern/cycles/blender/addon/operators.py index 63c9868ca6a..b53679bd328 100644 --- a/intern/cycles/blender/addon/operators.py +++ b/intern/cycles/blender/addon/operators.py @@ -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(): diff --git a/intern/cycles/blender/blender_python.cpp b/intern/cycles/blender/blender_python.cpp index 4a1eeeb65c1..ccd783073a3 100644 --- a/intern/cycles/blender/blender_python.cpp +++ b/intern/cycles/blender/blender_python.cpp @@ -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, ""}, diff --git a/intern/cycles/blender/blender_session.cpp b/intern/cycles/blender/blender_session.cpp index a06135b5362..cf856c3b3d4 100644 --- a/intern/cycles/blender/blender_session.cpp +++ b/intern/cycles/blender/blender_session.cpp @@ -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); diff --git a/intern/cycles/blender/blender_util.h b/intern/cycles/blender/blender_util.h index b9a1de08705..ec836bd5ec1 100644 --- a/intern/cycles/blender/blender_util.h +++ b/intern/cycles/blender/blender_util.h @@ -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); diff --git a/intern/cycles/render/CMakeLists.txt b/intern/cycles/render/CMakeLists.txt index e6afbc50463..b7c53f17c3d 100644 --- a/intern/cycles/render/CMakeLists.txt +++ b/intern/cycles/render/CMakeLists.txt @@ -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 index 00000000000..af655417046 --- /dev/null +++ b/intern/cycles/render/merge.cpp @@ -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 index 00000000000..48900a13c27 --- /dev/null +++ b/intern/cycles/render/merge.h @@ -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__ */ diff --git a/intern/cycles/test/CMakeLists.txt b/intern/cycles/test/CMakeLists.txt index 7e7e9af76e5..73fe590f8ae 100644 --- a/intern/cycles/test/CMakeLists.txt +++ b/intern/cycles/test/CMakeLists.txt @@ -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 index 00000000000..74f9f3b2134 --- /dev/null +++ b/intern/cycles/test/util_time_test.cpp @@ -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 diff --git a/intern/cycles/util/util_time.cpp b/intern/cycles/util/util_time.cpp index bc6ac4e2099..9983fdd1df3 100644 --- a/intern/cycles/util/util_time.cpp +++ b/intern/cycles/util/util_time.cpp @@ -14,15 +14,22 @@ * 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 diff --git a/intern/cycles/util/util_time.h b/intern/cycles/util/util_time.h index 13281bf188b..ed4dd5154d7 100644 --- a/intern/cycles/util/util_time.h +++ b/intern/cycles/util/util_time.h @@ -17,16 +17,20 @@ #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 |