Merge branch 'blender2.7'
This commit is contained in:
commit
fa59c6a3e7
|
@ -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():
|
||||
|
|
|
@ -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, ""},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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__ */
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
/* Time in format "hours:minutes:seconds.hundreds" */
|
||||
|
||||
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
|
||||
|
||||
#endif
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue