CategoriesPython

Generating a Starry Sky

Years ago, I discovered netpbm’s ppmforge, one option of which is to generate a starry sky, which fascinated me. The author’s website is full of interesting projects and papers, too. t I set out to rewrite it in Python, which I had just recently started learning at the time. Following is some code from 2012. If you want to compare it to ppmforge, the relevant lines are 63, 440-508. You’ll probably find that my younger self didn’t do a great job at translating everything to Python.

import random
import Image
import cStringIO
 
def cast(low, high):
    arand = (2.0**15.0) - 1.0
    return ((low)+(((high)-(low)) * ((random.randint(0, 12345678) & 0x7FFF) / arand)))
 
def make_star_image(width=600, height=600, star_fraction=3, star_quality=0.5, star_intensity=8, star_tint_exp=0.5, bg=None, lambd=0.0025):
    star_data = []
     
    if bg == None:
        star_image = Image.new("RGB", (width, height))
        for i in range(0, width):
            for l in range(0, height):
                if random.expovariate(1.5) < star_fraction:
                    v = int(star_intensity * ((1 / (1 - cast(0, 0.999))**star_quality)))
                    if v > 255:
                        v = 255
                    star_data.append((v, v, v))
                else:
                    star_data.append((0, 0, 0))
    else:
        index = 0
        if bg.mode != "RGB":
            bg = bg.convert("RGB")
        width, height = bg.size
        star_image = Image.new("RGB", (width, height))
        bg = bg.getdata()
        for i in range(0, width):
            for l in range(0, height):
                r, g, b = bg[index]
                average = (r + b + g) / 3
                r = random.expovariate(lambd)
                if r < average or random.random() > 0.9:
                    v = int(star_intensity * ((1 / (1 - cast(0, 0.999))**star_quality)))
                    if v > 255:
                        v = 255
                    if r < average:
                        if v > 40:
                            v = int(v * 1.5)
                            if v > 255:
                                v = 255
                            star_data.append((v, v, v))
                        elif v < 40 and random.random() > 0.5:
                            star_data.append((v, v, v))
                        else:
                            star_data.append((0, 0, 0))
                    else:
                        star_data.append((v, v, v))
                else:
                    star_data.append((0, 0, 0))
                index += 1
    star_image.putdata(star_data)
    return star_image
 
def main():
    make_star_image(width=1280, height=800, star_quality=1.2, lambd=0.0025, star_intensity=1, star_fraction=1).show()
    #bg = Image.open("/home/harrison/Pictures/wallpapers/bp1.jpg")
    #make_star_image(bg=bg, lambd=0.0035).show()
#    io = cStringIO.cStringIO()
#    starry.save(io, "JPEG")
#    data = io.getvalue()
#    
#    print "content-type: image/jpeg"
#    print
#    print data
 
if __name__ == "__main__":
    main()

The temperature calculation is off, the cast function isn’t quite right, and I didn’t even attempt the blackbody radiation calculations from the planck macro. The list could go on.

My most recent iteration of a starry sky generator is available on Github. This version is based on a deeper understanding of the math involved. For example, it turned out that the cast function was unnecessary, as its functionality is basically already built in to Python’s random module. I’m still working on understanding the planck function, so if you know much about blackbody radiation, I’d be happy to talk to you about it!

I like to compare these two versions of basically the same program, because it illustrates, in my mind, the idea of a Pythonic program. The improvements in the latest version are a result of better understanding both the problem and the solution.

The first version required the cast function because it just copied syntax over from the C program and made it work in Python. After taking the time to understand the problem that function was trying to solve, while also learning the best way to solve that problem in Python, it was able to be replaced entirely.

So, what would have been a troublesome function for a reader of the code to puzzle over, was turned into an easily-understood standard library function call.

Another example is in the control flow. The old version is deeply nested and confusing. The new one breaks more of it out into separate functions, uses clearer variable names, and makes better use of white space for grouping. I think it’s a lot easier to follow, besides looking prettier.

This comes up a lot when I’m solving code challenges on PyBites, too. When you compare solutions, it’s easy to see that some are better than others. Sometimes I’m happy with my solution, but sometimes it leaves a lot to be desired. It depends on how much experience I have with that kind of problem and how much time I put into understanding it.

The way to grow is to keep reading good examples of code and practicing. We can’t master every area, but we can keep improving!

There are several performance-related improvements in the new version, as well. For example, this version uses a bytearray to store the pixels before converting to an Image. In another post, I’ll go through the performance measurements I used to determine that this method is significantly faster than the other options.

I’m sure that there is still a lot that could be better about my latest starry sky generator, but it’s nice to be able to compare and see how much I’ve grown so far.

The Django app is currently hosted on Google AppEngine, so go ahead and check it out!

Or, see this page if you want to use text like the featured image.

Leave a Reply

Your email address will not be published.