360 Panoramas with Povray and/or ImageMagick
This is Part IV of a four-part article on ray tracing digital elevation model (DEM) data. The goal: render a ray-traced image of mountains from a digital elevation model (DEM).
- Part I, Get and Understand the Data: Working with Digital Elevation Models in Python
- Part II, Understand Povray: Height Fields in Povray
- Part III, Povray on real DEM data: Ray-Tracing Digital Elevation Data in 3D with Povray
- Part IV, Make a 3D Panorama: Making a Panorama from Raytraced DEM Images (this article)
Except there are actually several more parts on the way, related to using GRASS to make viewsheds. So maybe this is actually a five- or six-parter. We'll see.
The Easy Solution
Skipping to the chase here ... I had a whole long article written about how to make a sequence of images with povray, each pointing in a different direction, and then stitch them together with ImageMagick.
But a few days after I'd gotten it all working, I realized none of it was needed for this project, because ... ta-DA — povray accepts this argument inside its camera section:
angle 360
Duh! That makes it so easy.
You do need to change povray's projection to cylindrical; the default is "perspective" which warps the images. If you set your look_at to point due south -- the first and second coordinates are the same as your observer coordinate, the third being zero so it's looking southward -- then povray will create a lovely strip starting at 0 degrees bearing (due north), and with south right in the middle. The camera section I ended up with was:
camera { cylinder 1 location <0.344444, 0.029620, 0.519048> look_at <0.344444, 0.029620, 0> angle 360 }with the same light_source and height_field as in Part III.
There are still some more steps I'd like to do. For instance, fitting names of peaks to that 360-degree pan.
The rest of this article discusses some of the techniques I would have used, which might be useful in other circumstances.
A Script to Spin the Observer Around
Angles on a globe aren't as easy as just adding 45 degrees to the bearing angle each time. You need some spherical trigonometry to make the angles even, and it depends on the observer's coordinates.
Obviously, this wasn't something I wanted to calculate by hand, so I wrote a script for it: demproj.py. Run it with the name of a DEM file and the observer's coordinates:
demproj.py demfile.png 35.827 -106.1803It takes care of calculating the observer's elevation, normalizing to the image size and all that. It generates eight files, named outfileN.png, outfileNE.png etc.
Stitching Panoramas with ImageMagick
To stitch those demproj images manually in ImageMagick, this should work in theory:
convert -size 3600x600 xc:black \ outfile000.png -geometry +0+0 -composite \ outfile045.png -geometry +400+0 -composite \ outfile090.png -geometry +800+0 -composite \ outfile135.png -geometry +1200+0 -composite \ outfile180.png -geometry +1600+0 -composite \ outfile225.png -geometry +2000+0 -composite \ outfile270.png -geometry +2400+0 -composite \ outfile315.png -geometry +2800+0 -composite \ out-composite.pngor simply
convert outfile*.png +smush -400 out-smush.png
Adjusting Panoramas in GIMP
But in practice, some of the images have a few-pixel offset, and I never did figure out why; maybe it's a rounding error in my angle calculations.
I opened the images as layers in GIMP, and used my GIMP script Pandora/ to lay them out as a panorama. The cylindrical projection should make the edges match perfectly, so you can turn off the layer masking.
Then use the Move tool to adjust for the slight errors (tip: when the Move tool is active, the arrow keys will move the current layer by a single pixel).
If you get the offsets perfect and want to know what they are so you can use them in ImageMagick or another program, use GIMP's Filters->Python-Fu->Console. This assumes the panorama image is the only one loaded in GIMP, otherwise you'll have to inspect gimp.image_list() to see where in the list your image is.
>>> img = gimp.image_list()[0] >>> for layer in img.layers: ... print layer.name, layer.offsets
[ 15:28 Jul 23, 2019 More mapping | permalink to this entry | ]