Stacking photos with Python

Last month we went on vacations to a place with medium light pollution. It was also meteor shower season; the Perseids were peaking that week. So I decided to try some astro-photography. I bought a cheap barn door tracker, but it didn't arrive before we left, so I had to change my plans of taking Milky Way pictures.

I went for the other extreme; instead of steady stars, why not star trails? Luckily, my Nikon D7200 already has Interval Timer Shooting, so it was only a matter of setting it up correctly1 and let it take as many pictures as long as it still has power. You can say that was the easy part. The hard part was to do the stacking.

The way you stack star trail photos is that you combine them in Lighten mode. Ihad already used that technique to produce this photo Gimp can do it, if you load all the images as layers and then set each layer by hand with a lighten mode. The by hand part is the hard part. My first attempt gave me only 58 photos, so it was not that bad, but the repetitiveness of the task (select a layer, change the mode, iterate, a mouse only operation) asked for automation. Unluckily there is not a good way to generate a file that gimp can load with all that info prefilled; its native format, XCF, is binary, and embeds all the images, so 58x12Mpx4 channels equals 2784MB! Plus metadata... The second attempt yielded 250+ photos. Stacking that one was tedious, and the outcome (because of some technical reasons) was not that great.

Why not programmatically? I asked around what good Python modules that could do composition operation and I was pointed to GTK, but the API didn't look very friendly (to me it looks like it's more aimed to implement things like Inscape than GIMP). I looked for a second time and found BlendModes. That lead to a short script, but it was slow, and consumed lots of RAM. I didn't look into the code, but it's probably implemented in pure Python and might be doing some things wrong.

Third time's the charm: I found Image Blender. It's written in Cython, so it's mostly readable and fast, at least fast enough. I simply modified the original code and got this:

#! /usr/bin/python3

import sys
from PIL import Image
import image_blender

in_files = sys.argv[1:-1]
out_file = sys.argv[-1]
count = len(in_files)

out_data = None

for index, in_file in enumerate(in_files):
    print(f"[{index+1}/{count}] {in_file}")
    in_image = Image.open(in_file).convert(mode='RGBA')

    if out_data is None:
        out_data = Image.new('RGBA', in_image.size)

    new_out = Image.frombytes('RGBA', in_image.size, image_blender.lighten(out_data.tobytes(), in_image.tobytes()))

    in_image.close()
    out_data.close()

    out_data = new_out

out_data.convert('RGB').save(out_file)

Not pretty, but it does the work and fast enough! Faster than selecting 250+ layers in GIMP just to change the Mode :)


  1. I made myself a checklist, not complete:

    • Make sure the battery is full; use spare if needed.
    • Setup tripod with ballast for stability.
    • Large photo size (12Mp in my case).
    • Fine detail (I don't shoot RAW, don't have much time and expertise for developing photos, so low JPEG compression level).
    • No noise reduction.
    • VR off.
    • ISO between 400 and 1600; I think I took most with 800.
    • Manual Shooting Mode, 30s exposure2:, f as wide as possible; I used f/11.
    • Manual Focus.
    • Focus aiming at the brightest star, lens' zoom at peak (140mm in my case), then open back at the desired focal length, recompose.

  2. I wonder why all the cameras I have only allow exposures of up to 30s before going into bulb mode?