forked from bartvdbraak/blender
457 lines
13 KiB
C++
457 lines
13 KiB
C++
|
/*
|
||
|
* 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
|