<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>.:: Marcos Dione/StyXman's glob ::. (Posts about rasterio)</title><link>https://www.grulic.org.ar/~mdione/glob/</link><description></description><atom:link href="https://www.grulic.org.ar/~mdione/glob/categories/rasterio.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2025 &lt;a href="mailto:mdione@grulic.org.ar"&gt;Marcos Dione&lt;/a&gt; </copyright><lastBuildDate>Sat, 15 Nov 2025 20:52:05 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Automating blender based hillshading with Python</title><link>https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/</link><dc:creator>Marcos Dione</dc:creator><description>&lt;p&gt;Remember my &lt;a href="http://www.grulic.org.ar/~mdione/glob/posts/blender-hillshading-and-mapnik/"&gt;Blend based hillshading&lt;/a&gt;? I
promised to try to automate it, right? Well, it seems I have the interest and stamina now, so that's what I'm doing.
But boys and girls and anything in between and beyond, the stamina is waning and the culprit is Blender's internals
being exposed into a non-Pythonic API&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt;. I swear if I worked in anything remotely close to this, I would be writing a
wrapper for all this. But in the meantime, it's all a discovery path to something that does not resemble a
hack. Just read some of
&lt;a href="https://docs.blender.org/api/current/info_quickstart.html#data-creation-removal"&gt;Blender's Python Quickstart&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When you are familiar with other Python APIs you may be surprised that new data-blocks in the bpy API cannot be
created by calling the class:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nx"&gt;bpy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Mesh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;Traceback&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;most&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;recent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;blender_console&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bpy_struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__new__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;single&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;argument&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an intentional part of the API design. The Blender Python API can’t create Blender data that exists outside
the main Blender database (accessed through bpy.data), because this data is managed by Blender (save, load, undo,
append, etc).&lt;/p&gt;
&lt;p&gt;Data is added and removed via methods on the collections in bpy.data, e.g:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;mesh = bpy.data.meshes.new(name="MyMesh")
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is, instead of making the constructor call this internal API, they make it fail miserably and force you to use the
internal API! Today I was mentioning that Asterisk's programming language was definitely designed by a
Telecommunications Engineer, so I guess this one was designed by a 3D artist? But I digress...&lt;/p&gt;
&lt;p&gt;One of the first thing about Blender's internals is that one way to work is based on Contexts. This makes sense when
developing plugins, where you mostly need to apply things to the
selected object, but for someone really building everything from scratch like I need to, it feels weird.&lt;/p&gt;
&lt;p&gt;One of the advantages is that you can open a Python console and let Blender show you the calls it makes for every step
you make on the UI, but it's so context based that the results is useless as a script. Or for instance, linking the
output of a thing into he the input of another is registered as a drag-and-drop call that includes the distance the
mouse moved during the drag, so it's relative of the output dot where you started and what it links to also depends on
the physical and not logical position of the things you're linking,&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;bpy.ops.node.link(detach=False, drag_start=(583.898, 257.74))
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It takes quite a lot of digging around in a not very friendly REPL&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; with limited scrollback and
not much documentation to find more reproducible, less context dependent alternatives. This is what's eating up my
stamina, it's not so fun anymore. Paraphrasing someone on Mastodon: What use is a nice piece of Open Software if it's
documentation is not enough to be useful&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;?&lt;/p&gt;
&lt;p&gt;Another very important thing is that all objects have two views: one that has generic properties like position and
rotation, which can be reacheched by &lt;code&gt;bpy.data.objects&lt;/code&gt;; and one that has specific properties like a light's power or a
camera's lens angle, which can be reached by f.i. &lt;code&gt;bpy.data.cameras&lt;/code&gt;. This was utterly confusing, specially since
all &lt;code&gt;bpy.data&lt;/code&gt;'s documentation is &lt;strong&gt;4 lines long&lt;/strong&gt;. Later I found out you can get specific data from the generic one in
the &lt;code&gt;.data&lt;/code&gt; attribute, so the take out is: &lt;strong&gt;always get your objects from &lt;code&gt;bpy.data.objects&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Once we get over that issue, things are quite straightforward, but not necessarily easy. The script as it is can already
be used with &lt;code&gt;blender --background --python &amp;lt;script_file&amp;gt;&lt;/code&gt;, but have in account that when you do that, you start with
the default generic 3D setup, with a light, a camera and a cube. You have to delete the cube, but you can get a
reference to the other two to reuse them.&lt;/p&gt;
&lt;p&gt;Then comes the administrative stuff around just rendering the scene. To industrialize it and be able to quickly test
stuff, you can try to get command line options. You can use Python's &lt;code&gt;argparser&lt;/code&gt; module for this, but have in account
that those &lt;code&gt;--background --python blender.py&lt;/code&gt; options are going to be passed to the script, so you either ignore unknown
options or you declare those too:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;mdione&lt;/span&gt;&lt;span class="nv"&gt;@ioniq&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;elevation&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;
&lt;span class="n"&gt;Blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.6.2&lt;/span&gt;
&lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;"/home/mdione/.config/blender/3.6/config/userpref.blend"&lt;/span&gt;
&lt;span class="k"&gt;usage&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;-h&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;--render-samples RENDER_SAMPLES&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;--render-scale RENDER_SCALE&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;--height-scale HEIGHT_SCALE&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FILE&lt;/span&gt;
&lt;span class="nl"&gt;blender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unrecognized&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;--background --python&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Also, those options are going to be passed to Blender! So at the end of your run, Blender is going to complain that it
doesn't understand your options:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;unknown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;
&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Cannot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/mdione/src/projects/elevation/--render-samples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;such&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;
&lt;span class="n"&gt;Blender&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;quit&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The other step you should do is to copy the Geo part of GeoTIFF to the output file. I used &lt;code&gt;rasterio&lt;/code&gt;, mostly because at
first I tried &lt;code&gt;gdal&lt;/code&gt; (I was already using &lt;code&gt;gdal_edit.py&lt;/code&gt; to do this in my previous manual procedure), but it's API was
quite confusing and &lt;code&gt;rasterio&lt;/code&gt;'s is more plain. But, &lt;code&gt;rasterio&lt;/code&gt; can't actually open a file just to write the metadata
like &lt;code&gt;gdal&lt;/code&gt; does, so I had to open the output file, read all data, open it again for writing (this truncates the file)
and write metadata and data.&lt;/p&gt;
&lt;p&gt;Now, some caveats. First, as I advanced in my last post, the method as it is right now has issues at the seams. Blender
can't read GDAL VRT files, so either I build 9 planes instead of 1 (&lt;strong&gt;all&lt;/strong&gt; the neighbors are needed to properly
calculate the shadows because Blender is also taking in account light reflected back from other features, meaning
mountains) or for each 1x1 tile I generate another with some buffer. I will try the first one and see if it fixes this
issue without much runtime impact.&lt;/p&gt;
&lt;p&gt;Second, the script is not 100% parametrized. Sun size and power are fixed based on my tests. Maybe in the future. Third,
I will try to add a scattering sky, so we get a bluish tint to the shadows, and set the Sun's color to something
yellowish. These should probably be options too.&lt;/p&gt;
&lt;p&gt;Fourth, and probably most important. I discovered that this hillshading method is really sensible to missing or bad data,
because they look like dark, deep holes. This is probably a deal breaker for many, so you either fix your data, or you
search for better data, or you live with it. I'm not sure what I'm going to do.&lt;/p&gt;
&lt;p&gt;So, what did I do with this? Well, first, find good parameters, one for render samples and another for height scale.
Render time grows mostly linearly with render samples, so I just searched for the one before detail stopped appearing;
the value I found was 120 samples.
When we left off I was using 10 instead of 5 for height scale, but it looks too exaggerated on hills (but it looks
AWESOME in mountains like the Mount Blanc/Monte Bianco! See below), so I tried to pinpoint a good balance. For me it's 8,
maybe 7.&lt;/p&gt;
&lt;p&gt;Why get these values right? Because like I mentioned before, a single 1x1°, 3601x5137px tile takes some 40m in my laptop
at 100 samples, so the more tuned the better. One nice way to quickly test is to lower the samples or use the
&lt;code&gt;--render-scale&lt;/code&gt; option of the script to reduce the size of the output. Note that because you reduce both dimensions at
the same time, the final render (and the time that takes) is actually the square of this factor: 50% is actually 25%
(because 0.50 * 0.50 = 0.25).&lt;/p&gt;
&lt;p&gt;So, without further addo, here's my script. If you find it useful but want more power, open issues or PRs, everything
is welcome.&lt;/p&gt;
&lt;p&gt;https://github.com/StyXman/blender_hilllshading &lt;sup id="fnref:5"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fn:5"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Try to use the &lt;code&gt;main&lt;/code&gt; branch; &lt;code&gt;develop&lt;/code&gt; is considered unstable and can be broken.&lt;/p&gt;
&lt;p&gt;A couple of images of the shadows applied to my style as teaser, both using only 20 samples and x10 height scale:&lt;/p&gt;
&lt;p&gt;Dhaulagiri:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Dhaulagiri.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Mont Blanc/Monte Bianco:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/Mont_Blanc-Monte_Bianco.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Man, I love the fact that the tail of the Giacchiaio del Miage is in shadows, but the rest is not; or how
Monte Bianco/Mont Blanc's shadow reaches across the valley to the base of la Tête d'Arp. But also notice the bad data
close to la Mer de Glace.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Ok, TBH here, I'm very much used to &lt;code&gt;ipython&lt;/code&gt;'s console, it's really closer to the plain &lt;code&gt;python&lt;/code&gt; one. No tab
completion, so lots of calls to &lt;code&gt;dir()&lt;/code&gt; and a few &lt;code&gt;help()&lt;/code&gt;s. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;I couldn't find it again. Mastodon posts are not searchable by default, which I understand is good for privacy, but
on the other hand the current clients don't store anything locally, so you can't even search what you already saw.
I have several semi-ranting posts about this and I would show them to you, but they got lost on Mastodon. See what I
mean? &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;So you have an idea, this took me a whole week of free time to finish, including but not in the text, my old nemesis,
terracing effect. This thing is &lt;strong&gt;brittle&lt;/strong&gt;. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Yeah, maybe the API is mostly designed for this. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:4" title="Jump back to footnote 4 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;My site generator keeps breaking. This is the second time I have to publicly admit this. Maybe next weekend I'll
gather steam and replace it with &lt;code&gt;nikola&lt;/code&gt;. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/#fnref:5" title="Jump back to footnote 5 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>blender</category><category>dem</category><category>elevation</category><category>gdal</category><category>gis</category><category>hillshading</category><category>openstreetmap</category><category>python</category><category>rasterio</category><guid>https://www.grulic.org.ar/~mdione/glob/posts/automating-blender-based-hillshading-with-python/</guid><pubDate>Sun, 05 Nov 2023 16:19:45 GMT</pubDate></item><item><title>Trying to calculate proper shading</title><link>https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/</link><dc:creator>Marcos Dione</dc:creator><description>&lt;p&gt;It all started with the following statement: "my renders are too slow". There
were three issues as the root case: osm-carto had become more complex (nothing I
can do there), my compute resources were too nimble (but upgrading them would
cost money I don't want to spend in just one hobby) and the terrain rasters were being
reprojected all the time.&lt;/p&gt;
&lt;p&gt;My style uses three raster layers to provide a sense of relief: one providing
height tints, one enhancing that color on slopes and desaturating it on valleys,
and a last one providing hill shading. All three were derived from the same
source DEM, which is projected in &lt;a href="https://epsg.io/4326"&gt;WGS 84 a.k.a. EPSG 4326&lt;/a&gt;.
The style is of course in
&lt;a href="https://epsg.io/3857"&gt;Pseudo/Web Mercator, a.k.a. EPSG 3857&lt;/a&gt;; hence the constant
reprojection.&lt;/p&gt;
&lt;p&gt;So my first reaction was: reproject first, then derive the raster layers. This
single step made it all hell break loose.&lt;/p&gt;
&lt;p&gt;I usually render Europe. It's the region where I live and where I most travel to.
I'm using relief layers because I like mountains, so I'm constantly checking how
they look. And with this change, the mountains up North (initially was around
North Scandinavia) looked all washed out:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/compensating-washed_out.png"&gt;&lt;/p&gt;
&lt;p&gt;The reason behind this is that GDAL's hill and slope shading
algorithms don't take in account the projection, but just use pixel values as present
in the dataset and the declared pixel size. The DEMs I'm using are of a resolution
of 1 arc second per pixel. WGS84 assumes a ellipsoid of 6_378_137m of radius (plus
a flattening parameter which we're going to ignore), which means a circumference
at the Equator of:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;math&lt;/span&gt;
&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6_378_137&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# in meters&lt;/span&gt;
&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;circunf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# 2πr&lt;/span&gt;
&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;circunf&lt;/span&gt;
&lt;span class="n"&gt;Out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;40_075_016.68557849&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# in meters&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One arc second is hence this 'wide':&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;In [5]: circunf / 360 / 60 / 60
Out[5]: 30.922080775909325  # in meters
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In fact the DEMs advertises exactly that:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;mdione&lt;/span&gt;&lt;span class="nv"&gt;@diablo&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;osm&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mapzen&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gdalinfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;N79E014&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hgt&lt;/span&gt;
&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3601&lt;/span&gt;
&lt;span class="n"&gt;Coordinate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;System&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;GEOGCRS&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;"WGS 84",&lt;/span&gt;
&lt;span class="n"&gt;    DATUM["World Geodetic System 1984",&lt;/span&gt;
&lt;span class="n"&gt;        ELLIPSOID["WGS 84",6378137,298.257223563,&lt;/span&gt;
&lt;span class="n"&gt;            LENGTHUNIT["metre",1&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PRIMEM&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;"Greenwich",0,&lt;/span&gt;
&lt;span class="n"&gt;        ANGLEUNIT["degree",0.0174532925199433&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CS&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ellipsoidal,2&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;AXIS&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;"geodetic latitude (Lat)",north,&lt;/span&gt;
&lt;span class="n"&gt;            ORDER[1&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;ANGLEUNIT&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;"degree",0.0174532925199433&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;AXIS&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;"geodetic longitude (Lon)",east,&lt;/span&gt;
&lt;span class="n"&gt;            ORDER[2&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;ANGLEUNIT&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;"degree",0.0174532925199433&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;"EPSG",4326&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;Pixel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.000277777777778&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.000277777777778&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Well, not &lt;em&gt;exactly&lt;/em&gt; that, but we can derive it 'easily'. Deep down there, the
declared unit is &lt;code&gt;degree&lt;/code&gt; (ignore the &lt;code&gt;metre&lt;/code&gt; in the datum; that's the unit for
the numbers in the &lt;code&gt;ELLIPSOID&lt;/code&gt;) and its size in radians (just in case you aren't
satisfied with the amount of conversions involved, I guess). The projection declares
degrees as the unit of the projection for both axis, and the pixel size is declared in the units of the
projection, and 1 arc second is:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;In [6]: 1 / 60 / 60  # or 1/3600
Out[6]: 0.0002777777777777778  # in degrees
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which is exactly the value declared in the pixel size&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;. So one pixel is one
arc second and one arc second is around 30m &lt;em&gt;at the Equator&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The 'flatness' problem arises because all these projections assume all latitudes are of the
same width; for them, a degree at the Equator is as 'wide' as one at 60°N, which is not; in reality,
it's &lt;em&gt;twice&lt;/em&gt; as wide, because the width of a degree decreases with the cosine of the latitude and
&lt;code&gt;math.cos(math.radians(60)) == 0.5&lt;/code&gt;. The
mountains we saw in the first image are close to 68N; the cosine up there is:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;In [7]: math.cos(math.radians(68))
Out[7]: 0.37460659341591196
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So pixels up there are no longer around 30m wide, but only about
&lt;code&gt;30 * 0.37 ≃ 11  # in meters&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;How does this relate to the flatness? Well, like I said, GDAL's algorithms use the
declared pixel size &lt;em&gt;everywhere&lt;/em&gt;, so at f.i. 68°N it thinks the pixel is around 30m
wide when it's only around 11m wide. They're miscalculating by a factor of
almost 3! Also, the Mercator projection is famous for stretching not only in the
East-West direction (longitude) but also in latitude, at the same pace, based on the
inverse of the cosine of the latitude:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;stretch_factor = 1 / math.cos(math.radians(lat))
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So all the mountains up there 'look' to the algorithms as 3 times wider &lt;em&gt;and&lt;/em&gt; 'taller'
('tall' here means in the latitude, N-S direction, not height AMSL), hence the washing out.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/compensating-stretching.png"&gt;&lt;/p&gt;
&lt;p&gt;In that image we have an original mountain 10 units wide and 5 units tall; but &lt;code&gt;gdal&lt;/code&gt;
thinks it's 20 units wide, so it looks flat.&lt;/p&gt;
&lt;p&gt;Then it hit me: it doesn't matter what projection you are using, unless you
calculate slopes and hill shading on the &lt;em&gt;sphere&lt;/em&gt; (or spheroid, but we all want
to keep it simple, right? Otherwise we wouldn't be using WebMerc all over :),
the numbers are going to be &lt;strong&gt;wrong&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The Wandering Cartographer
&lt;a href="https://wanderingcartographer.wordpress.com/2017/11/27/making-shaded-relief-from-dems/"&gt;mentions this&lt;/a&gt;
and goes around the issue by doing two things: Always using DEMs where the units
are meters, and always using a local projection that doesn't deform as much. He can do that
because he produces small maps. He also concedes he is
&lt;a href="https://wanderingcartographer.wordpress.com/2021/04/30/making-shaded-relief-directly-from-dems-projected-in-degrees/"&gt;using the 111_120 constant&lt;/a&gt;
when the unit is degrees.
I'm not really sure about how that number is calculated, but I suspect that it
has something to do with the flattening parameter, and also some level of
rounding. It seems to have originated from
&lt;a href="http://manpages.ubuntu.com/manpages/impish/en/man1/gdaldem.1.html#modes"&gt;&lt;code&gt;gdaldem&lt;/code&gt;'s manpage&lt;/a&gt;
(search for the &lt;code&gt;-s&lt;/code&gt; option). Let's try something:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;In [8]: m_per_degree = circunf / 360
In [9]: m_per_degree
Out[9]: 111_319.49079327358  # in meters
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's how wide is a degree is at the Equator. Close enough, I guess.&lt;/p&gt;
&lt;p&gt;But for us world-wide, or at least continent-wide webmappers like me, those suggestions
are not enough; we can't choose a projection that doesn't deform because at
those scales they all forcibly do, and we can't use a single scaling factor either.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-analyst-toolbox/how-hillshade-works.htm#ESRI_SECTION1_F4EF4BBFAB5E4072BE6876F2F2428695"&gt;&lt;code&gt;gdal&lt;/code&gt;'s algorithm&lt;/a&gt;
uses the 8 neighboring pixels of a pixel to determine slope and aspect. The slope
is calculated on a sum of those pixels' values divided by the cell size. We can't change the
cell size that &lt;code&gt;gdal&lt;/code&gt; uses, but we can change the &lt;em&gt;values&lt;/em&gt; :) That's the third triangle up there: the algorithm
assumes a size for the base that it's &lt;code&gt;X&lt;/code&gt; times bigger than the real one, and we just make the
triangle as many times taller. &lt;code&gt;X&lt;/code&gt; is just &lt;code&gt;1 / math.cos(math.radians(lat))&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If we apply this technique at &lt;em&gt;each&lt;/em&gt; DEM file individually, we have to chose a latitude for it. One option is
to take the latitude at the center of the file. It would make your code from more
complicated, specially if you're using &lt;code&gt;bash&lt;/code&gt; or, worse, &lt;code&gt;make&lt;/code&gt; because you have to pass a
different value for &lt;code&gt;-s&lt;/code&gt;, but you can always use a little bit of Python or any other high level,
interpreted language.&lt;/p&gt;
&lt;p&gt;But what happens at the seams where one tile meets another? Well, since each file has it's own
latitude, two neighbouring values, one in one DEM file and the other in another, will have different
compensation values. So, when you jump from a ~0.39 factor to a ~0.37 at the 68° of latitude, you start
to notice it, and these are not the mountains more to the North that there are.&lt;/p&gt;
&lt;p&gt;The next step would be to start cutting DEM tiles into smaller ones, so the 'jumps'
in between are less noticeable... but can we make this more continuous-like? Yes we can!
We simply compensate each pixel based on its latitude.&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="https://www.grulic.org.ar/~mdione/glob/images/compensating-final_EU_DEM.png"&gt;&lt;/p&gt;
&lt;p&gt;This image cheats a little; this one is based on EU-DEM and not mapzen like the original.
You can see where the DEM file ends because the slope and hillshading effects stop at the bottom
left.
I still have to decide which dataset I will use; at some point I decided that EU-DEM is not
global enough for me, and that it's probably better to use a single dataset (f.i. mapzen)
than a more accurate but partial one, but since I usually render the regions I need,
I might as well change my workflow to easily change the DEM dataset.&lt;/p&gt;
&lt;p&gt;This approach works fine as shown, but has at least one limitation. The data type used in those TIFFs
is &lt;code&gt;Int16&lt;/code&gt;. This means &lt;em&gt;signed&lt;/em&gt; ints 16 bits wide, which means values can go between -32768..32767.
We could only compensate Everest up to less than 4 times, but luckily we don't have to; it
would have to be at around 75° of latitude before the compensated value was bigger than the
maximum the type can represent. But at around 84° we get a compensation of 10x
and it only gets quickly worse from there:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;3.863&lt;/span&gt;
&lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;4.133&lt;/span&gt;
&lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;4.445&lt;/span&gt;
&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;4.809&lt;/span&gt;
&lt;span class="mi"&gt;79&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;5.240&lt;/span&gt;
&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;5.758&lt;/span&gt;
&lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;6.392&lt;/span&gt;
&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;7.185&lt;/span&gt;
&lt;span class="mi"&gt;83&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;8.205&lt;/span&gt;
&lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mf"&gt;9.566&lt;/span&gt;
&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;11.474&lt;/span&gt;
&lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14.336&lt;/span&gt;
&lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;19.107&lt;/span&gt;
&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;28.654&lt;/span&gt;
&lt;span class="mi"&gt;89&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;57.299&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;we&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;don&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;far&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cutout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;around&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;85.5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IIRC&lt;/span&gt;
&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;∞&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The solution could be to use a floating point type, but that means all calculations would
be more expensive, but definitely not as much as reprojecting on the fly. Or maybe we can use
a bigger integer type. Both solutions would also use more disk space and require more memory.
&lt;code&gt;gdal&lt;/code&gt; currently supports band types of Byte, UInt16, Int16, UInt32, Int32, Float32, Float64,
CInt16, CInt32, CFloat32 and CFloat64 for reading and writing.&lt;/p&gt;
&lt;p&gt;Another thing is that it works fine and quick for datasets like SRTM and mapzen because I can
compensate whole raster lines in one go as all its pixels have the same latitude, but for
EU-DEM I have to compensate &lt;em&gt;every pixel&lt;/em&gt; and it becomes &lt;em&gt;slow&lt;/em&gt;. I could try to parallelize it
(I bought a new computer which has 4x the CPUs I had before, and 4x the RAM too :), but I first
have to figure out how to slowly write TIFFS; currently I have to hold the whole output TIFF in
memory before dumping it into a file.&lt;/p&gt;
&lt;p&gt;Here's the code for pixel per pixel processing; converting it to do row by row for f.i. mapzen
makes you actually remove some code :)&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#! /usr/bin/env python3&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;math&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;rasterio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pyproj&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt;

&lt;span class="n"&gt;in_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rasterio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;band&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# yes, we assume only one band&lt;/span&gt;

&lt;span class="n"&gt;out_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rasterio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'GTiff'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtypes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                         &lt;span class="n"&gt;crs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;out_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="n"&gt;transformer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pyproj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transformer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_crs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'epsg:4326'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# scan every line in the input&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# EU-DEM does not provide 'nice' 1x1 rectangular tiles,&lt;/span&gt;
    &lt;span class="c1"&gt;# they use a specific projection that become 'arcs' in anything 'rectangular'&lt;/span&gt;

    &lt;span class="c1"&gt;# so, pixel by pixel&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# rasterio does not say where do the coords returned fall in the pixel&lt;/span&gt;
        &lt;span class="c1"&gt;# but at 30m tops, we don't care&lt;/span&gt;
        &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# convert back to latlon&lt;/span&gt;
        &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# calculate a compensation value based on lat&lt;/span&gt;
        &lt;span class="c1"&gt;# real widths are pixel_size * cos(lat), we compensate by the inverse of that&lt;/span&gt;
        &lt;span class="n"&gt;coef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;coef&lt;/span&gt;

    &lt;span class="n"&gt;out_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# save in a new file.&lt;/span&gt;
&lt;span class="n"&gt;out_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numpy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtypes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's it!&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Except for the negative value. That has to do with the fact that pixels count
from top to bottom, but degrees from South (negative) to North. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;The other number, 0.0174532925199433, is how much a degree is in radians. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/#fnref:2" title="Jump back to footnote 2 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;All that extra math is converting degrees into radians so &lt;code&gt;cos()&lt;/code&gt; is happy. &lt;a class="footnote-backref" href="https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/#fnref:3" title="Jump back to footnote 3 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>crs</category><category>dem</category><category>elevation</category><category>gdal</category><category>gis</category><category>hillshading</category><category>openstreetmap</category><category>pyproj</category><category>python</category><category>rasterio</category><guid>https://www.grulic.org.ar/~mdione/glob/posts/trying-to-calculate-proper-shading/</guid><pubDate>Sat, 26 Mar 2022 08:52:37 GMT</pubDate></item></channel></rss>