How to combine low sample renders?

I am rendering my scene with only one sample but all the renders of the same picture have a different seed value.
How can I combine them correctly?
I wrote a java-program that loads all the images and takes the average pixel color for each pixel of the images. Is that a correct way of combining the images? It doesn't reduce the noise very much so I am not happy with it. What can I do?

• – p2or Jan 6 '15 at 15:22

Update:

A friend of mine wrote a little tool that does exactly this. I used it in my latest project and it is way better than the old script in this answer I used before.

The old answer is slightly incorrect, because it uses non-linear sRGB colors for its calculations. See here for how to convert them to linear space and back to fix the program.

Found your question by accident. I created a small Java script for exactly that purpose some time ago:

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

public class Cycles {

public static final String  BLENDER_PATH    = "C:\\Users\\%USERNAME%\\Documents\\blender";
// Be nice and do not change this one
public static final String  EXT             = "png";

public Cycles() {
}

/** Lazy way for zero-padding numbers */
private static String toString(int number) {
return "" + (number < 1000 ? ("0" + (number < 100 ? ("0" + (number < 10 ? ("0" + number) : number)) : number)) : number);
}

/**
* @param folder Path to the image relative to {@link Cycles#BLENDER_PATH} excluding file extension and frame number
* @param start First frame of combining range (inclusive)
* @param end Last frame of combining range (inclusive)
* @param save Save the final image to this path
*/
private static void combine(String folder, int start, int end, String save) throws IOException {
final BufferedImage[] pics = new BufferedImage[end - start + 1];
int num = -1;
for (int i = 0; i < pics.length; i++) {
try {
pics[i] = ImageIO.read(new File(BLENDER_PATH + folder + toString(i + start) + "." + EXT));
if (num == -1)
num = i;
System.out.println("Image " + (i + start));
} catch (IOException e) {
}
}
System.out.println("Done. Calculating...");
BufferedImage pic = new BufferedImage(pics[num].getWidth(), pics[num].getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int x = 0; x < pic.getWidth(); x++)
for (int y = 0; y < pic.getHeight(); y++) {
int count = 0;
int r = 0, g = 0, b = 0;
for (int i = 0; i < pics.length; i++) {
if (pics[i] == null)
continue;
Color c = new Color(pics[i].getRGB(x, y));
int alpha = (pics[i].getRGB(x, y) >> 24) & 0xff;
r += c.getRed() * alpha / 255;
g += c.getGreen() * alpha / 255;
b += c.getBlue() * alpha / 255;
count++;
}
pic.setRGB(x, y, new Color(r / count, g / count, b / count).getRGB());
}
System.out.println("Saving to " + save + "...");
ImageIO.write(pic, EXT, new File("C:\\Users\\Poseidon\\Documents\\blender\\" + folder + save + "." + EXT));
System.out.println("Done.");
}

public static void main(String[] args) throws IOException {
String path = "projectFolder\\image";
// 100 images don't fit in RAM - split in 4 parts
combine(path, 1, 25, "0101"); // 1-25 --> 101
combine(path, 26, 50, "0102");// 26-50 --> 102
combine(path, 51, 75, "0103"); // 51-75 --> 103
combine(path, 76, 100, "0104"); // 76-100 --> 104

combine(path, 101, 104, "-1"); // 101-104 --> -1 (final)
}
}


It's not command-line, so to use it change the values in Eclipse directly.

The process that I've seen is combining each image in Blender via a Mix node, and using a mix percentage based on [INCORRECT]---- how many images are being combined (in the example he uses 20% since there are 5 images) ----[/INCORRECT]. See the attached screen shot from Kent Trammell's tutorial on CG Cookie.

EDIT: Actually, Ben Simmonds corrected Kent's math on this tutorial. Here's a quote from Ben on the page:

HI Kent. I think your math was incorrect for those mix nodes. If you think about how each image contributes to the final comp, the first image node in that chain is contributing 4x more than the 2nd to the final image (0.8 vs 0.2). The correct values for the mix nodes in such a chain would be as follows:

1st and 2nd: 50% (1st plus 2nd) and 3rd: 33% (1st, 2nd and 3rd) and 4th: 25% (1st through 4th) and 5th: 20%

If you were mixing the images all at once then the 20% per image value would be correct, but the math is slightly different for mixing them pairwise.

Alternatively, the simpler option would be to ADD all of the images together, and then divide the pixel values of the resulting image by 5.

• Thanks for your answer. But what when I have for example 100 images with a low amount of samples? It comes hard to combine them in blender. That's why I am looking for a solution in java... – rudolf97 Jan 6 '15 at 13:58
• I just updated the answer with the corrected math. Kent's math original tutorial was actually incorrect. See Ben's comments about 2 ways to combine the images. I think you should be able to replicate the combination process in your java app. – Todd McIntosh Jan 6 '15 at 14:21

Update: refer again to piegames' answer which is updated not only with a better tool, but also additional info regarding linear vs non-linear color spaces that I was unaware of. Luckily this does not appear to have had much impact in my case, though I was working with few colors and relatively little dynamic range.

I came across this answer looking for the same thing, and because my use case is probably far from unique, I thought it best to update with another solution.

My problem: I use a rendering service to create longer animations, but the cheapest tier of the service allows only 15 minutes of render time per frame. So, I have many directories representing the entire animation rendered at fairly low samples with a different seed for each, rather than many of a still image in one directory. Combining these would give me the ability to generate a high quality animation without having to upgrade to a more expensive tier.

I've updated @piegames answer for myself, and multithreaded it because churning through 700 frames on one core was maddeningly slow in this case. Note that this update also preserves the alpha channel. (requires Java 8+)

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;

public class Cycles {

// My files are in  this heirarchy: "/mnt/easystore/WM/WM all frames seed 1/WM_0300.png"
public static final String  BLENDER_PATH    = "/mnt/easystore/WM/";
// Be nice and don't change this one
public static final String  EXT             = "png";
public static final String  FOLDER_PREFIX   = "WM all frames seed ";
public static final String  IMAGE_PREFIX    = "WM_";
public static final String  OUTPUT_FOLDER   = "out/";
public static final int     NUM_RENDERS     = 5;
public static final int     START_FRAME     = 300;
public static final int     END_FRAME       = 1000;
public static final int     THREADS         = 6;

public Cycles() {
}

/** Lazy way for zero-padding numbers */
private static String toString(int number) {
return "" + (number < 1000 ? ("0" + (number < 100 ? ("0" + (number < 10 ? ("0" + number) : number)) : number)) : number);
}

/**
* @param frame Which frame number to combine from each full animation directory
*/
private static void combine(int frame) throws IOException {
final BufferedImage[] pics = new BufferedImage[NUM_RENDERS];
for (int i = 0; i < NUM_RENDERS; i++) {
try {
pics[i] = ImageIO.read(new File(BLENDER_PATH + FOLDER_PREFIX + Integer.toString(i) + "/" + IMAGE_PREFIX + toString(frame) + "." + EXT));
System.out.println("Reading frame " + (frame) + " image " + (i));
} catch (IOException e) {
}
}
BufferedImage pic = new BufferedImage(pics[0].getWidth(), pics[0].getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int x = 0; x < pic.getWidth(); x++) {
for (int y = 0; y < pic.getHeight(); y++) {
int count = 0;
int r = 0, g = 0, b = 0, a = 0;
for (int i = 0; i < pics.length; i++) {
if (pics[i] == null)
continue;
Color c = new Color(pics[i].getRGB(x, y));
r += c.getRed();
g += c.getGreen();
b += c.getBlue();
a += c.getAlpha();
count++;
}
pic.setRGB(x, y, new Color(r/count, g/count, b/count, a/count).getRGB());
}
}
String filename = BLENDER_PATH + OUTPUT_FOLDER + IMAGE_PREFIX + toString(frame) + "." + EXT;
System.out.println("Saving to " + filename);
ImageIO.write(pic, EXT, new File(filename));
System.out.println("Finished frame " + frame + ".");
}

public static void main(String[] args) throws IOException, InterruptedException {

java.util.stream.IntStream frames = java.util.stream.IntStream.range(START_FRAME, END_FRAME + 1);
frames.forEach( i -> {
Runnable r = new Runnable() {
public void run() {
try {
combine(i);
} catch (IOException e) { // lol let's not worry about it
}
}
};
pool.execute(r);
});
pool.awaitTermination(30, TimeUnit.SECONDS);
pool.shutdown();
}

}
$$$$
`