/* * 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 #include 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 channel_names; /* Offsets of layer channels in image. */ vector channel_offsets; /* Type of operation to perform when merging. */ vector channel_ops; /* Sample amount that was used for rendering this layer. */ int samples; }; /* Merge Image */ class MergeImage { public: /* OIIO file handle. */ unique_ptr in; /* Image file path. */ string filepath; /* Render layers. */ vector 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& layers, string& error) { const std::vector &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 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::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& filepaths, vector& images, string& error) { for(const string& filepath: filepaths) { unique_ptr 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& 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& 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& 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& 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 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 images; if(!open_images(input, images, error)) { return false; } /* Merge pixels. */ array merge_pixels; vector 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 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