7
$\begingroup$

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?

$\endgroup$
2
$\begingroup$

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.

Old answer:

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 {

    // Insert username here
    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];
        System.out.println("Reading images..");
        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.

$\endgroup$
6
$\begingroup$

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.

Multiseed Rendered Image Combination

$\endgroup$
  • 1
    $\begingroup$ 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... $\endgroup$ – rudolf97 Jan 6 '15 at 13:58
  • 1
    $\begingroup$ 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. $\endgroup$ – Todd McIntosh Jan 6 '15 at 14:21
2
$\begingroup$

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 {

        ExecutorService pool = Executors.newFixedThreadPool(THREADS);

        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();
    }

}
```
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.