<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.flightgear.org/w/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Wlbragg</id>
	<title>FlightGear wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.flightgear.org/w/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Wlbragg"/>
	<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/Special:Contributions/Wlbragg"/>
	<updated>2026-06-17T20:07:54Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.39.6</generator>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144924</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144924"/>
		<updated>2026-06-06T03:37:01Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
This version is used when combining NLCD Tree Canopy rastors with NLCD Land Cover rastors&lt;br /&gt;
&lt;br /&gt;
See all notes in code if not using Tree Canopy rasters.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2016&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alaska&amp;quot;&lt;br /&gt;
# The following two parameters are now arguments, see ARGUMENT PARSING below&lt;br /&gt;
# PART = &amp;quot;89-86_29&amp;quot;&lt;br /&gt;
# CANOPY = 41&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
#PERCENTAGE = 11.0&lt;br /&gt;
#SIZE = 21&lt;br /&gt;
PERCENTAGE = 8.0&lt;br /&gt;
SIZE = 11&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivelent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step0_strip_nodata_from_canopy():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}_NoMask.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst, [&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, &amp;quot;none&amp;quot;,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, &amp;quot;TILED=YES&amp;quot;,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, &amp;quot;COMPRESS=LZW&amp;quot;,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, &amp;quot;ALPHA=NO&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 0: Removed NoData + mask from canopy&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}_NoMask.tiff&amp;quot;)&lt;br /&gt;
    #src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step1_5_strip_mask_from_reclassed_canopy():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined_NoMask.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst, [&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, &amp;quot;none&amp;quot;,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, &amp;quot;TILED=YES&amp;quot;,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, &amp;quot;COMPRESS=LZW&amp;quot;,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, &amp;quot;ALPHA=NO&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1.5: Removed mask from reclassed canopy&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined_NoMask.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdalwarp(src, dst, extra=[&lt;br /&gt;
        &amp;quot;-srcnodata&amp;quot;, &amp;quot;none&amp;quot;,&lt;br /&gt;
        &amp;quot;-dstnodata&amp;quot;, &amp;quot;none&amp;quot;,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, &amp;quot;ALPHA=NO&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        f&amp;quot;((B &amp;gt; 0) &amp;amp; (A != 41) &amp;amp; (A != 42) &amp;amp; (A != 43)) * B + &amp;quot;&lt;br /&gt;
        f&amp;quot;((B &amp;lt;= 0) | (A == 41) | (A == 42) | (A == 43)) * A&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    # Replace below&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    # with this&lt;br /&gt;
    #src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    # when not combining Land Cover and Tree Canopy&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10b_clamp_values():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Clamped_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;(A&amp;gt;=1)*(A&amp;lt;=44)*A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 10b: Smoothed-HD-Clamped_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Clamped_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
# When not combining Tree Canopy with Land Cover,&lt;br /&gt;
# comment out step0, step1, step1_5, step2 and step4&lt;br /&gt;
# and replace src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
# with src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
# in step5.&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step0_strip_nodata_from_canopy()&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step1_5_strip_mask_from_reclassed_canopy()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step10b_clamp_values()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144921</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144921"/>
		<updated>2026-06-05T22:40:43Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
This version is used when combining NLCD Tree Canopy rastors with NLCD Land Cover rastors&lt;br /&gt;
&lt;br /&gt;
See all notes in code if not using Tree Canopy rasters.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
# The following two parameters are now arguments, see ARGUMENT PARSING below&lt;br /&gt;
# PART = &amp;quot;89-86_29&amp;quot;&lt;br /&gt;
# CANOPY = 41&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def get_extent(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    x0 = gt[0]&lt;br /&gt;
    y0 = gt[3]&lt;br /&gt;
    x1 = x0 + gt[1] * ds.RasterXSize&lt;br /&gt;
    y1 = y0 + gt[5] * ds.RasterYSize&lt;br /&gt;
    return [x0, y1, x1, y0]&lt;br /&gt;
&lt;br /&gt;
def get_size(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    return [ds.RasterXSize, ds.RasterYSize]&lt;br /&gt;
&lt;br /&gt;
def warp_canopy_to_landcover_extent():&lt;br /&gt;
    canopy = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    land = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    out = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdalwarp(&lt;br /&gt;
        canopy,&lt;br /&gt;
        out,&lt;br /&gt;
        t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
        resampling=&amp;quot;near&amp;quot;,&lt;br /&gt;
        dtype=&amp;quot;Byte&amp;quot;,&lt;br /&gt;
        extra = [&amp;quot;-te&amp;quot;] + [str(v) for v in get_extent(land)] + [&amp;quot;-ts&amp;quot;] + [str(v) for v in get_size(land)] + [&amp;quot;-srcnodata&amp;quot;, &amp;quot;0&amp;quot;, &amp;quot;-dstnodata&amp;quot;, &amp;quot;0&amp;quot;]&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        # Use canopy where canopy exists&lt;br /&gt;
        &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0) &amp;amp; (B &amp;lt; 255) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43)) * A + &amp;quot;&lt;br /&gt;
        # Use land cover where canopy is 0 OR canopy is NoData OR land cover is forest&lt;br /&gt;
        &amp;quot;((A &amp;lt;= 0) | (A != A) | (B == 41) | (B == 42) | (B == 43)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    # replace &lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    # with&lt;br /&gt;
    #src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    # if not combining tree canopy raster with land cover raster &lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
# Comment out step1, warp_canopy_to_landcover_extent(), and step4 below&lt;br /&gt;
# and in step5, replace &lt;br /&gt;
# src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
# with&lt;br /&gt;
# src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
# if not combining tree canopy raster with land cover raster&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326() &lt;br /&gt;
    warp_canopy_to_landcover_extent()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step10b_clamp_values()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144920</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144920"/>
		<updated>2026-06-05T22:40:24Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
This version is used when combining NLCD Tree Canopy rastors with NLCD Land Cover rastors&lt;br /&gt;
&lt;br /&gt;
See all notes in code if not using Tree Canopy rasters.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
# The following two parameters are now arguments, see ARGUMENT PARSING below&lt;br /&gt;
# PART = &amp;quot;89-86_29&amp;quot;&lt;br /&gt;
# CANOPY = 41&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def get_extent(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    x0 = gt[0]&lt;br /&gt;
    y0 = gt[3]&lt;br /&gt;
    x1 = x0 + gt[1] * ds.RasterXSize&lt;br /&gt;
    y1 = y0 + gt[5] * ds.RasterYSize&lt;br /&gt;
    return [x0, y1, x1, y0]&lt;br /&gt;
&lt;br /&gt;
def get_size(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    return [ds.RasterXSize, ds.RasterYSize]&lt;br /&gt;
&lt;br /&gt;
def warp_canopy_to_landcover_extent():&lt;br /&gt;
    canopy = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    land = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    out = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdalwarp(&lt;br /&gt;
        canopy,&lt;br /&gt;
        out,&lt;br /&gt;
        t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
        resampling=&amp;quot;near&amp;quot;,&lt;br /&gt;
        dtype=&amp;quot;Byte&amp;quot;,&lt;br /&gt;
        extra = [&amp;quot;-te&amp;quot;] + [str(v) for v in get_extent(land)] + [&amp;quot;-ts&amp;quot;] + [str(v) for v in get_size(land)] + [&amp;quot;-srcnodata&amp;quot;, &amp;quot;0&amp;quot;, &amp;quot;-dstnodata&amp;quot;, &amp;quot;0&amp;quot;]&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        # Use canopy where canopy exists&lt;br /&gt;
        &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0) &amp;amp; (B &amp;lt; 255) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43)) * A + &amp;quot;&lt;br /&gt;
        # Use land cover where canopy is 0 OR canopy is NoData OR land cover is forest&lt;br /&gt;
        &amp;quot;((A &amp;lt;= 0) | (A != A) | (B == 41) | (B == 42) | (B == 43)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    # replace &lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    # with&lt;br /&gt;
    #src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    # if not combining tree canopy raster with land cover raster &lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
# Comment out step1, warp_canopy_to_landcover_extent(), and step4 below&lt;br /&gt;
# and in step5, replace &lt;br /&gt;
# src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
# with&lt;br /&gt;
# src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
# if not combining tree canopy raster with land cover raster&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326() &lt;br /&gt;
    warp_canopy_to_landcover_extent()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step10b_clamp_values()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144919</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144919"/>
		<updated>2026-06-05T22:35:32Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
# The following two parameters are now arguments, see ARGUMENT PARSING below&lt;br /&gt;
# PART = &amp;quot;89-86_29&amp;quot;&lt;br /&gt;
# CANOPY = 41&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def get_extent(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    x0 = gt[0]&lt;br /&gt;
    y0 = gt[3]&lt;br /&gt;
    x1 = x0 + gt[1] * ds.RasterXSize&lt;br /&gt;
    y1 = y0 + gt[5] * ds.RasterYSize&lt;br /&gt;
    return [x0, y1, x1, y0]&lt;br /&gt;
&lt;br /&gt;
def get_size(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    return [ds.RasterXSize, ds.RasterYSize]&lt;br /&gt;
&lt;br /&gt;
def warp_canopy_to_landcover_extent():&lt;br /&gt;
    canopy = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    land = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    out = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdalwarp(&lt;br /&gt;
        canopy,&lt;br /&gt;
        out,&lt;br /&gt;
        t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
        resampling=&amp;quot;near&amp;quot;,&lt;br /&gt;
        dtype=&amp;quot;Byte&amp;quot;,&lt;br /&gt;
        extra = [&amp;quot;-te&amp;quot;] + [str(v) for v in get_extent(land)] + [&amp;quot;-ts&amp;quot;] + [str(v) for v in get_size(land)] + [&amp;quot;-srcnodata&amp;quot;, &amp;quot;0&amp;quot;, &amp;quot;-dstnodata&amp;quot;, &amp;quot;0&amp;quot;]&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        # Use canopy where canopy exists&lt;br /&gt;
        &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0) &amp;amp; (B &amp;lt; 255) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43)) * A + &amp;quot;&lt;br /&gt;
        # Use land cover where canopy is 0 OR canopy is NoData OR land cover is forest&lt;br /&gt;
        &amp;quot;((A &amp;lt;= 0) | (A != A) | (B == 41) | (B == 42) | (B == 43)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    # replace &lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    # with&lt;br /&gt;
    #src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    # if not combining tree canopy raster with land cover raster &lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
# Comment out step1, warp_canopy_to_landcover_extent(), and step4 below&lt;br /&gt;
# and in step5, replace &lt;br /&gt;
# src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
# with&lt;br /&gt;
# src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
# if not combining tree canopy raster with land cover raster&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326() &lt;br /&gt;
    warp_canopy_to_landcover_extent()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step10b_clamp_values()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144918</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144918"/>
		<updated>2026-06-05T22:25:57Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
# The following two parameters are now arguments, see ARGUMENT PARSING below&lt;br /&gt;
# PART = &amp;quot;89-86_29&amp;quot;&lt;br /&gt;
# CANOPY = 41&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def get_extent(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    x0 = gt[0]&lt;br /&gt;
    y0 = gt[3]&lt;br /&gt;
    x1 = x0 + gt[1] * ds.RasterXSize&lt;br /&gt;
    y1 = y0 + gt[5] * ds.RasterYSize&lt;br /&gt;
    return [x0, y1, x1, y0]&lt;br /&gt;
&lt;br /&gt;
def get_size(path):&lt;br /&gt;
    ds = gdal.Open(path)&lt;br /&gt;
    return [ds.RasterXSize, ds.RasterYSize]&lt;br /&gt;
&lt;br /&gt;
def warp_canopy_to_landcover_extent():&lt;br /&gt;
    canopy = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    land = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    out = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdalwarp(&lt;br /&gt;
        canopy,&lt;br /&gt;
        out,&lt;br /&gt;
        t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
        resampling=&amp;quot;near&amp;quot;,&lt;br /&gt;
        dtype=&amp;quot;Byte&amp;quot;,&lt;br /&gt;
        extra = [&amp;quot;-te&amp;quot;] + [str(v) for v in get_extent(land)] + [&amp;quot;-ts&amp;quot;] + [str(v) for v in get_size(land)] + [&amp;quot;-srcnodata&amp;quot;, &amp;quot;0&amp;quot;, &amp;quot;-dstnodata&amp;quot;, &amp;quot;0&amp;quot;]&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_Aligned_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        # Use canopy where canopy exists&lt;br /&gt;
        &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0) &amp;amp; (B &amp;lt; 255) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43)) * A + &amp;quot;&lt;br /&gt;
        # Use land cover where canopy is 0 OR canopy is NoData OR land cover is forest&lt;br /&gt;
        &amp;quot;((A &amp;lt;= 0) | (A != A) | (B == 41) | (B == 42) | (B == 43)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    warp_canopy_to_landcover_extent()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step10b_clamp_values()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144552</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144552"/>
		<updated>2026-05-21T15:53:06Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Generating the Terrain using osgdem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic [[WS3.0]] terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the Windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can add the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note: the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
--coastline requires OSM data mask from here [https://osmdata.openstreetmap.de/data/land-polygons.html OSM Shapefiles] Make sure you download the shapefiles on this page and not the alternate &amp;quot;Water polygons or Coastlines&amp;quot;. It is the land cover shapefiles that are used as the cutting mask, not the water line data.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA in the QGIS Python Editor ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
=== Python script plus Grass to process NLCD data (also in batch) ===&lt;br /&gt;
&lt;br /&gt;
[[Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
Note: --tile-image-size 256 is the default tile size produced when using genVPB.py. The hi-res rasters produced by the NLCD or Sentinel-2 Python3 processing scripts will not match that resolution in the final scenery unless this flag is set to --tile-image-size 1024. But the tradeoff is a heavier scenery that might not run on lower spec systems.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Virtual_Planet_Builder&amp;diff=144551</id>
		<title>Virtual Planet Builder</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Virtual_Planet_Builder&amp;diff=144551"/>
		<updated>2026-05-21T15:46:24Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Running VPB */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
{{Scenery Core Development}}&lt;br /&gt;
&lt;br /&gt;
VirtualPlanetBuilder (VPB) is an OSG tool that generates native OSG data to represent terrain, usually by draping a GEO-TIFF over an elevation model.&lt;br /&gt;
&lt;br /&gt;
== Building VirtualPlanetBuilder ==&lt;br /&gt;
&lt;br /&gt;
The VPB project and code can be found here: https://github.com/openscenegraph/VirtualPlanetBuilder&lt;br /&gt;
&lt;br /&gt;
There is now[https://sourceforge.net/p/flightgear/mailman/message/37385201/] a VPB {{Wikipedia|Docker_(software)|docker}} container for Linux: https://hub.docker.com/r/flightgear/ws30-vbp-generator and https://github.com/fdalvi/flightgear-ws30-vbp-generator&lt;br /&gt;
&lt;br /&gt;
If you've got an existing FlightGear build, there are some additional dependencies:&lt;br /&gt;
* OSG&lt;br /&gt;
** Including NVTT.  ('sudo apt-get install libnvtt-dev' - oddly VPB builds without it but then checks for the shared library at runtime...) &lt;br /&gt;
* GDAL  (https://gdal.org/),  Note that this needs to 2.4.X&lt;br /&gt;
** ... which requires PROJ 6 or above (https://proj.org/)&lt;br /&gt;
*** ... which requires the sqlite3.11 or above development package (on linux - 'sudo apt-get install libsqlite3-dev')&lt;br /&gt;
&lt;br /&gt;
Note that GDAL and PROJ do not use cmake, so it builds in the source directory as follows:&lt;br /&gt;
 ./configure -prefix=/path/to/install&lt;br /&gt;
 make&lt;br /&gt;
&lt;br /&gt;
VPB is built using cmake e.g.&lt;br /&gt;
&lt;br /&gt;
 cmake -DOSG_DIR=/path/to/OSG/install /path/to/VPB/src&lt;br /&gt;
 make&lt;br /&gt;
&lt;br /&gt;
== Getting Landcover Data ==&lt;br /&gt;
&lt;br /&gt;
For CORINE data covering Europe, try here:  https://land.copernicus.eu/pan-european/corine-land-cover/clc2018&lt;br /&gt;
&lt;br /&gt;
For US National Landcover Database  (NLCD) try here: https://www.mrlc.gov/viewer/&lt;br /&gt;
&lt;br /&gt;
ESRI (akak Sentinel-2) covers the entire world but with fewer distinct landclasses: https://livingatlas.arcgis.com/landcoverexplorer.&lt;br /&gt;
&lt;br /&gt;
Choose RASTER data, which will download a geo-referenced TIF image.&lt;br /&gt;
&lt;br /&gt;
Note that the mapping from landcover to materials is performed in Materials/base/landclass-mapping.xml, and currently only maps CORINE raster landclasses.  If you have another landcover source (e.g. the US NLCD. ESRI), you will need to use your GIS (Grass, QGIS) to convert them such that the raster uses the correct landclass indexes.&lt;br /&gt;
&lt;br /&gt;
=== Using Vector Data ===&lt;br /&gt;
If you wish to create higher resolution scenery, you may need to convert from vector data. &lt;br /&gt;
&lt;br /&gt;
Key points:&lt;br /&gt;
&lt;br /&gt;
* If required, re-project to WGS84.  Ensure you project and layer are in the same projection.&lt;br /&gt;
* Map the vector polygons to have the correct landclass.  In QGIS, you can do this as follows:&lt;br /&gt;
** Open the Attribute Table&lt;br /&gt;
** Create a new field (c_index) with type int&lt;br /&gt;
** Open the field calculator the update the new field (c_index) using a calculation that maps from the landclass fields in your data.  For CORINE vector data this is the c18 field.  An example is below, and some mappings are available [https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/ here]:&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
CASE&lt;br /&gt;
WHEN c18=111 THEN 1&lt;br /&gt;
WHEN c18=112 THEN 2&lt;br /&gt;
WHEN c18=121 THEN 3&lt;br /&gt;
WHEN c18=122 THEN 4&lt;br /&gt;
WHEN c18=123 THEN 5&lt;br /&gt;
WHEN c18=124 THEN 6&lt;br /&gt;
.&lt;br /&gt;
.&lt;br /&gt;
.&lt;br /&gt;
END&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Use a rasterize command to create a raster:&lt;br /&gt;
&lt;br /&gt;
 gdal_rasterize -l CORINE_Modified_WGS84 -a Landclass -ts 35000.0 33300.0 -a_nodata 48.0 -te 3.0 51.0 8.0 54.0 -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/Modified/CORINE_Modified_WGS84.gpkg /home/stuart/FlightGear/VPB/data/10m/Netherlands2.tif&lt;br /&gt;
&lt;br /&gt;
== Getting DEM data. == &lt;br /&gt;
&lt;br /&gt;
The most commonly used Digital Elevation Model used is SRTM.&lt;br /&gt;
&lt;br /&gt;
SRTM with 90m resolution is available here: http://srtm.csi.cgiar.org/&lt;br /&gt;
&lt;br /&gt;
SRTM with 30m resolution is available here: https://lpdaac.usgs.gov/products/nasadem_hgtv001/ ''([https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/ Directory] , [https://search.earthdata.nasa.gov/search Interactive search] by selecting areas or points: [https://search.earthdata.nasa.gov/search/granules?p=C1546314043-LPDAAC_ECS&amp;amp;pg&amp;amp;#x5B;0&amp;amp;#x5D;&amp;amp;#x5B;v&amp;amp;#x5D;=f&amp;amp;pg&amp;amp;#x5B;0&amp;amp;#x5D;&amp;amp;#x5B;gsk&amp;amp;#x5D;=-start_date&amp;amp;q=NASADEM%20HGT&amp;amp;tl=1633679290.379!3!! example] . To download you need a free account from [https://urs.earthdata.nasa.gov/users/new here]'')&lt;br /&gt;
&lt;br /&gt;
VPB accepts both raster and HGT files.&lt;br /&gt;
&lt;br /&gt;
== LoD, Mesh size and Framerate ==&lt;br /&gt;
&lt;br /&gt;
Unlike WS2.0 which has a fixed terrain mesh, VPB uses different terrain meshes with different level of detail (LoD) depending on the distance of the viewer from the particular piece of terrain.    &lt;br /&gt;
[[File:Virtual Planet Builder LoD.png|thumb|Simplified diagram of different LoD tiles being loaded as an aircraft gets closer.]]    &lt;br /&gt;
[[File:VPB Lod Filename scheme.png|thumb|Diagram showing how the Virtual Planet Builder filenames are built for each LoD level.  Note that this just shows 3 LoD Levels.  Our scenery goes to &amp;quot;L5&amp;quot;.]]&lt;br /&gt;
&lt;br /&gt;
Virtual Planet Builder provides a variety of settings that have a big impact on the eventual terrain mesh.  The two main settings (the number of LoD levels, and the ratio of the LoD radius and maximum visible range), have a huge impact on the number of vertices, how the terrain looks, LoD &amp;quot;popping&amp;quot;, and framerate.    &lt;br /&gt;
&lt;br /&gt;
To attempt to understand the relationship, a test was performed.  WS30 terrain was generated for a 2x2 degree section of the European Alps bounded by (7E, 45N), and (8E, 47N), with settings as listed below.  FlightGear was then run on a desktop with an NVidia GeForce GTX 1660 with 6GB of VRAM.  The system appeared I/O limited as GPU utilization was around 30% and on 2GB of VRAM was used.  &lt;br /&gt;
&lt;br /&gt;
The following commandline options were used to place the ufo just to the NE of the Matterhorn, facing SW.  &lt;br /&gt;
 --aircraft=ufo --timeofday=morning --disable-real-weather-fetch --disable-ai-traffic --disable-random-objects --disable-random-vegetation --altitude=15000 --prop:/scenery/use-vpb=true --disable-sound --disable-ai-models --prop:/sim/rendering/texture-cache/cache-enabled=false --lat=46.021023 --lon=7.728291 --altitude=12662.75 --heading=226.0 &lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
Experimental results varying LoD Level and visibility distance ratios&lt;br /&gt;
!Dem Resolution&lt;br /&gt;
!LoD Levels&lt;br /&gt;
!Radius to max visible distance ratio&lt;br /&gt;
!Lowest LoD Radius (km)&lt;br /&gt;
!Highest LoD Range (km)&lt;br /&gt;
!Lowest LoD Range (km)&lt;br /&gt;
!Vertices in view&lt;br /&gt;
!Triangles in view (M)&lt;br /&gt;
!Framerate (M)&lt;br /&gt;
!Frame Latency (ms)&lt;br /&gt;
!Terrain Size (MB)&lt;br /&gt;
|-&lt;br /&gt;
|90&lt;br /&gt;
|8&lt;br /&gt;
|7&lt;br /&gt;
|0.6&lt;br /&gt;
|1099&lt;br /&gt;
|4.3&lt;br /&gt;
|15&lt;br /&gt;
|30&lt;br /&gt;
|28&lt;br /&gt;
|40&lt;br /&gt;
|128&lt;br /&gt;
|-&lt;br /&gt;
|30&lt;br /&gt;
|8&lt;br /&gt;
|7&lt;br /&gt;
|0.6&lt;br /&gt;
|1099&lt;br /&gt;
|4.3&lt;br /&gt;
|32&lt;br /&gt;
|61&lt;br /&gt;
|19&lt;br /&gt;
|59&lt;br /&gt;
|425&lt;br /&gt;
|-&lt;br /&gt;
|30&lt;br /&gt;
|7&lt;br /&gt;
|5&lt;br /&gt;
|1.2&lt;br /&gt;
|785&lt;br /&gt;
|6.1&lt;br /&gt;
|5.6&lt;br /&gt;
|11&lt;br /&gt;
|75&lt;br /&gt;
|17&lt;br /&gt;
|170&lt;br /&gt;
|-&lt;br /&gt;
|30&lt;br /&gt;
|7&lt;br /&gt;
|3&lt;br /&gt;
|1.2&lt;br /&gt;
|475&lt;br /&gt;
|3.7&lt;br /&gt;
|3&lt;br /&gt;
|6&lt;br /&gt;
|105&lt;br /&gt;
|14&lt;br /&gt;
|170&lt;br /&gt;
|-&lt;br /&gt;
|30&lt;br /&gt;
|6&lt;br /&gt;
|5&lt;br /&gt;
|2.5&lt;br /&gt;
|785&lt;br /&gt;
|12&lt;br /&gt;
|4&lt;br /&gt;
|7&lt;br /&gt;
|115&lt;br /&gt;
|13 (a)&lt;br /&gt;
|80&lt;br /&gt;
|-&lt;br /&gt;
|30&lt;br /&gt;
|4&lt;br /&gt;
|5&lt;br /&gt;
|9.8&lt;br /&gt;
|785&lt;br /&gt;
|49&lt;br /&gt;
|3&lt;br /&gt;
|6&lt;br /&gt;
|125&lt;br /&gt;
|12 (b)&lt;br /&gt;
|25&lt;br /&gt;
|}&lt;br /&gt;
Notes:&lt;br /&gt;
&lt;br /&gt;
(a) - Using these settings there was a noticeable reduction in apparent heightmap resolution with a 30m DEM, probably because the resolution of the lowest LoD level was ~40m, compared with 20m for an 7 LoD level mesh.  &lt;br /&gt;
&lt;br /&gt;
(b) Using these settings, the terrain resolution was unacceptably poor.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
These results suggest that using 7 LoD Levels and a radius of &amp;quot;3&amp;quot; will provide a good balance, with high terrain quality and good frame-rate.&lt;br /&gt;
&lt;br /&gt;
== Running VPB ==&lt;br /&gt;
&lt;br /&gt;
FlightGear expects scenery in 1x1 degree tiles in a slightly different format to WS2.0.  The scenery needs to be in a &amp;quot;vpb&amp;quot; directory, followed by a directory structure similar to WS20 (10x10, then 1x1 degree directories), with the final file being a 1x1 degree section with a &amp;quot;ws_&amp;quot; prefix, and in the &amp;quot;.osgb&amp;quot; format.  E.g. vpb/w010n50/w004n50/ws_w004n50.osgb&lt;br /&gt;
&lt;br /&gt;
VPB is available in a docker image [https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/ws30-vbp-generator-docker/ here].  Instructions on how to install and use it can be found in [[Howto:Create WS3.0 terrain]].&lt;br /&gt;
&lt;br /&gt;
Here are the options I've used to generate some scenery of the UK:&lt;br /&gt;
&lt;br /&gt;
 ./bin/osgdem --TERRAIN \&lt;br /&gt;
 --image-ext png&lt;br /&gt;
 --no-interpolate-imagery \&lt;br /&gt;
 --disable-error-diffusion \&lt;br /&gt;
 --geocentric \&lt;br /&gt;
 -t /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif \&lt;br /&gt;
 -d /home/stuart/FlightGear/VPB/data/SRTM90/srtm_34_02.tif \&lt;br /&gt;
 -d /home/stuart/FlightGear/VPB/data/SRTM90/srtm_35_01.tif \&lt;br /&gt;
 -d /home/stuart/FlightGear/VPB/data/SRTM90/srtm_35_02.tif \&lt;br /&gt;
 -d /home/stuart/FlightGear/VPB/data/SRTM90/srtm_36_01.tif \&lt;br /&gt;
 -d /home/stuart/FlightGear/VPB/data/SRTM90/srtm_36_02.tif \&lt;br /&gt;
 -b -4 50 -3 51 \&lt;br /&gt;
 --PagedLOD \&lt;br /&gt;
 -l 7 \&lt;br /&gt;
 --radius-to-max-visible-distance-ratio 3 \&lt;br /&gt;
 -o vpb/w010n50/w004n50/ws_w004n50.osgb&lt;br /&gt;
&lt;br /&gt;
* --TERRAIN to use osgTerrain::Terrain database&lt;br /&gt;
* --compressor-nvtt --compression-quality-highest to generate native DDS mipmaps&lt;br /&gt;
* --geocentric because that's what FG uses&lt;br /&gt;
* -t the texture to drape (in this case CORINE)&lt;br /&gt;
* -d the DEM to use (in this case pieces of SRTM90)&lt;br /&gt;
* -b is the extents in decimal degrees: LON1 LAT1 LON2 LAT2, in this case from (-4,50) to (-3,51)&lt;br /&gt;
* --PagedLOD to generate PagedLOD nodes&lt;br /&gt;
* -l 4 the number of LOD levels to generate. At the equator, a 1x1 degree tile has a 157km diagonal. The LoD follows a quad-tree like structure, with each LOD level having 4 sub-tiles in a 2x2 array.  If we want our smallest tile to have a ~10km diagonal, we need 4 LoD levels&lt;br /&gt;
*  --radius-to-max-visible-distance-ratio is self-explanatory.  We want the lowest level of LoD to be visible 20km away, so a ratio of 5 is appropriate.&lt;br /&gt;
* -o is the output file, in this case native OSG binary format.  FG expects WS3.0 data in a vpb subdirectory, with the structure shown.&lt;br /&gt;
&lt;br /&gt;
NOTE: --tile-image-size 256 is the default and the tile size used with genVPB.py. It is not possible to realize the hi-res edge you achieve with the NLCD processing scripts. That requires a --tile-image-size 1024 or greater. The tradeoff though, is a much larger scenery footprint which could possibly too heavy for lower spec systems.&lt;br /&gt;
[[Category:Scenery software]]&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144541</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144541"/>
		<updated>2026-05-20T17:24:38Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
# The following two parameters are now arguments, see ARGUMENT PARSING below&lt;br /&gt;
# PART = &amp;quot;89-86_29&amp;quot;&lt;br /&gt;
# CANOPY = 41&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + &amp;quot;&lt;br /&gt;
        &amp;quot;((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144537</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144537"/>
		<updated>2026-05-20T17:22:55Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
# Now an argument, see ARGUMENT PARSING below&lt;br /&gt;
# PART = &amp;quot;89-86_29&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + &amp;quot;&lt;br /&gt;
        &amp;quot;((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144536</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144536"/>
		<updated>2026-05-20T17:19:41Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Python script to process NLCD for the USA */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic [[WS3.0]] terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the Windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can add the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note: the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
--coastline requires OSM data mask from here [https://osmdata.openstreetmap.de/data/land-polygons.html OSM Shapefiles] Make sure you download the shapefiles on this page and not the alternate &amp;quot;Water polygons or Coastlines&amp;quot;. It is the land cover shapefiles that are used as the cutting mask, not the water line data.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA in the QGIS Python Editor ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
=== Python script plus Grass to process NLCD data (also in batch) ===&lt;br /&gt;
&lt;br /&gt;
[[Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144535</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144535"/>
		<updated>2026-05-20T17:14:47Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Batch script for multiple processes */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Python script to process NLCD for the USA in the QGIS Python Console ===&lt;br /&gt;
You can process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution. &lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144534</id>
		<title>Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)&amp;diff=144534"/>
		<updated>2026-05-20T17:14:40Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: Created page with &amp;quot;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===  This has been tested in Linux, it may or may not need modification to run under Windows OS.  USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41  &amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt; #!/usr/bin/env python3 import os import subprocess import argparse import tempfile from pathlib import Path from osgeo import gdal  # ----------------- CONFIG -----------------  BASE_PATH =...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + &amp;quot;&lt;br /&gt;
        &amp;quot;((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144533</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144533"/>
		<updated>2026-05-20T17:14:09Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Python script to process NLCD for the USA in the QGIS Python Console ===&lt;br /&gt;
You can process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution. &lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144532</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144532"/>
		<updated>2026-05-20T17:12:21Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Python script to process Sentinel-2 data */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic [[WS3.0]] terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the Windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can add the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note: the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
--coastline requires OSM data mask from here [https://osmdata.openstreetmap.de/data/land-polygons.html OSM Shapefiles] Make sure you download the shapefiles on this page and not the alternate &amp;quot;Water polygons or Coastlines&amp;quot;. It is the land cover shapefiles that are used as the cutting mask, not the water line data.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
=== Python script plus Grass to process NLCD data (also in batch) ===&lt;br /&gt;
&lt;br /&gt;
[[Pure_Python_script_to_process_NLCD_for_the_USA_(does_not_require_using_QGIS_Python_Console)]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144531</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144531"/>
		<updated>2026-05-20T17:02:34Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Python script to process NLCD for the USA in the QGIS Python Console ===&lt;br /&gt;
You can process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution. &lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) ===&lt;br /&gt;
&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
import argparse&lt;br /&gt;
import tempfile&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS binary&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# ----------------- ARGUMENT PARSING -----------------&lt;br /&gt;
&lt;br /&gt;
# USAGE: python3 gen-scenery.py --part 89-86_29 --canopy 41&lt;br /&gt;
&lt;br /&gt;
#NLCD&lt;br /&gt;
# Choose the dominent canopy for the area being processed&lt;br /&gt;
# Areas not covered in the Tree Canopy layer will convert retain their original class&lt;br /&gt;
#41 = DeciduousForest&lt;br /&gt;
#42 = EvergreenForest&lt;br /&gt;
#43 = MixedForest&lt;br /&gt;
&lt;br /&gt;
#FG equivalent &lt;br /&gt;
#22 = AgroForest&lt;br /&gt;
#23 = DeciduousBroadCover&lt;br /&gt;
#24 = EvergreenForest&lt;br /&gt;
#25 = MixedForest&lt;br /&gt;
&lt;br /&gt;
parser = argparse.ArgumentParser()&lt;br /&gt;
parser.add_argument(&amp;quot;--part&amp;quot;, required=True, help=&amp;quot;Tile part ID, e.g. 89-86_29&amp;quot;)&lt;br /&gt;
parser.add_argument(&amp;quot;--canopy&amp;quot;, required=True, type=int, help=&amp;quot;Tree canopy FG class&amp;quot;)&lt;br /&gt;
args = parser.parse_args()&lt;br /&gt;
&lt;br /&gt;
PART = args.part&lt;br /&gt;
CANOPY = args.canopy&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
# ----------------- GRASS SESSION -----------------&lt;br /&gt;
&lt;br /&gt;
# Create a temporary GRASS location with PERMANENT mapset&lt;br /&gt;
TMPDIR = tempfile.mkdtemp(prefix=&amp;quot;grass_job_&amp;quot;)&lt;br /&gt;
GRASS_LOCATION = os.path.join(TMPDIR, &amp;quot;location&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# Create the location&lt;br /&gt;
subprocess.run([&lt;br /&gt;
    GRASS_BIN,&lt;br /&gt;
    &amp;quot;-c&amp;quot;, &amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
    GRASS_LOCATION,&lt;br /&gt;
    &amp;quot;--exec&amp;quot;, &amp;quot;echo&amp;quot;, &amp;quot;Location created&amp;quot;&lt;br /&gt;
], check=True)&lt;br /&gt;
&lt;br /&gt;
# The active mapset is PERMANENT&lt;br /&gt;
GRASS_MAPSET = os.path.join(GRASS_LOCATION, &amp;quot;PERMANENT&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    cmd = [GRASS_BIN, GRASS_MAPSET, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = f&amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * {CANOPY} + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + &amp;quot;&lt;br /&gt;
        &amp;quot;((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*2 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*3&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Batch script for multiple processes ==&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
python3 gen-scenery.py --part 89-86_29 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_30 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_31 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_32 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_33 --canopy 41 &amp;amp;&lt;br /&gt;
python3 gen-scenery.py --part 89-86_34 --canopy 41 &amp;amp;&lt;br /&gt;
&lt;br /&gt;
wait&lt;br /&gt;
echo &amp;quot;All scenery jobs completed.&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144521</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144521"/>
		<updated>2026-05-19T22:08:07Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Pure Python script to process NLCD for the USA (does not require using QGIS Python Console) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Python script to process NLCD for the USA in the QGIS Python Console ===&lt;br /&gt;
You can process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution. &lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)===&lt;br /&gt;
&lt;br /&gt;
This has been tested in Linux, it may or may not need modification to run under Windows OS.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
PART = &amp;quot;89-86_32&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS config (adjust to your setup)&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;              # e.g. &amp;quot;grass&amp;quot;, &amp;quot;grass82&amp;quot;, or full path&lt;br /&gt;
GISDBASE = &amp;quot;/path/to/grassdata&amp;quot;  # directory containing locations&lt;br /&gt;
LOCATION_NAME = &amp;quot;nlcd&amp;quot;           # existing location name&lt;br /&gt;
MAPSET = &amp;quot;PERMANENT&amp;quot;             # or a dedicated mapset&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixes malformed args like '--type=Byte' and always appends --overwrite.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def grass_env():&lt;br /&gt;
    env = os.environ.copy()&lt;br /&gt;
    env[&amp;quot;GISDBASE&amp;quot;] = GISDBASE&lt;br /&gt;
    env[&amp;quot;LOCATION_NAME&amp;quot;] = LOCATION_NAME&lt;br /&gt;
    env[&amp;quot;MAPSET&amp;quot;] = MAPSET&lt;br /&gt;
    return env&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    FIXED: list-based wrapper that preserves argument order exactly.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    env = grass_env()&lt;br /&gt;
    cmd = [GRASS_BIN, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True, env=env)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Import GeoTIFF -&amp;gt; set region -&amp;gt; r.neighbors -&amp;gt; export GeoTIFF.&lt;br /&gt;
    Includes -f to allow DCELL-&amp;gt;Byte export.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1) import&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    # 2) set region to raster (fixes extent issues)&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    # 3) neighbors&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    # 4) export (with -f)&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + &amp;quot;&lt;br /&gt;
        &amp;quot;((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*2&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144520</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144520"/>
		<updated>2026-05-19T22:03:11Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Optional Python script to process NLCD for the USA */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Python script to process NLCD for the USA in the QGIS Python Console ===&lt;br /&gt;
You can process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution. &lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Pure Python script to process NLCD for the USA (does not require using QGIS Python Console)===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import subprocess&lt;br /&gt;
from pathlib import Path&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
&lt;br /&gt;
# ----------------- CONFIG -----------------&lt;br /&gt;
&lt;br /&gt;
BASE_PATH = Path(&amp;quot;/media/wayne/TOSHIBA-EXT/Scenery/ws3.0&amp;quot;)&lt;br /&gt;
YEAR = &amp;quot;2023&amp;quot;&lt;br /&gt;
STATE = &amp;quot;Alabama&amp;quot;&lt;br /&gt;
PART = &amp;quot;89-86_32&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# upsampling / smoothing&lt;br /&gt;
PERCENTAGE = 11.0&lt;br /&gt;
SIZE = 21&lt;br /&gt;
&lt;br /&gt;
# GRASS config (adjust to your setup)&lt;br /&gt;
GRASS_BIN = &amp;quot;grass&amp;quot;              # e.g. &amp;quot;grass&amp;quot;, &amp;quot;grass82&amp;quot;, or full path&lt;br /&gt;
GISDBASE = &amp;quot;/path/to/grassdata&amp;quot;  # directory containing locations&lt;br /&gt;
LOCATION_NAME = &amp;quot;nlcd&amp;quot;           # existing location name&lt;br /&gt;
MAPSET = &amp;quot;PERMANENT&amp;quot;             # or a dedicated mapset&lt;br /&gt;
&lt;br /&gt;
# ----------------- HELPERS -----------------&lt;br /&gt;
&lt;br /&gt;
def run(cmd, check=True, env=None):&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(str(c) for c in cmd))&lt;br /&gt;
    subprocess.run(cmd, check=check, env=env)&lt;br /&gt;
&lt;br /&gt;
def gdal_calc(args):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Fixes malformed args like '--type=Byte' and always appends --overwrite.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    fixed = []&lt;br /&gt;
    for a in args:&lt;br /&gt;
        if a.startswith(&amp;quot;--&amp;quot;) and &amp;quot;=&amp;quot; in a:&lt;br /&gt;
            key, val = a.split(&amp;quot;=&amp;quot;, 1)&lt;br /&gt;
            fixed.append(key)&lt;br /&gt;
            fixed.append(val)&lt;br /&gt;
        else:&lt;br /&gt;
            fixed.append(a)&lt;br /&gt;
&lt;br /&gt;
    cmd = [&amp;quot;gdal_calc.py&amp;quot;] + fixed + [&amp;quot;--overwrite&amp;quot;]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdalwarp(input_path, output_path, t_srs=&amp;quot;EPSG:4326&amp;quot;,&lt;br /&gt;
             resampling=&amp;quot;near&amp;quot;, dtype=&amp;quot;Byte&amp;quot;, extra=None):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdalwarp&amp;quot;,&lt;br /&gt;
        &amp;quot;-t_srs&amp;quot;, t_srs,&lt;br /&gt;
        &amp;quot;-r&amp;quot;, resampling,&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    if extra:&lt;br /&gt;
        cmd.extend(extra)&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def gdal_translate(input_path, output_path,&lt;br /&gt;
                   nodata=0, dtype=&amp;quot;Byte&amp;quot;, compress=&amp;quot;LZW&amp;quot;):&lt;br /&gt;
    cmd = [&lt;br /&gt;
        &amp;quot;gdal_translate&amp;quot;,&lt;br /&gt;
        &amp;quot;-a_nodata&amp;quot;, str(nodata),&lt;br /&gt;
        &amp;quot;-ot&amp;quot;, dtype,&lt;br /&gt;
        &amp;quot;-co&amp;quot;, f&amp;quot;COMPRESS={compress}&amp;quot;,&lt;br /&gt;
        &amp;quot;-overwrite&amp;quot;,&lt;br /&gt;
        input_path,&lt;br /&gt;
        output_path&lt;br /&gt;
    ]&lt;br /&gt;
    run(cmd)&lt;br /&gt;
&lt;br /&gt;
def grass_env():&lt;br /&gt;
    env = os.environ.copy()&lt;br /&gt;
    env[&amp;quot;GISDBASE&amp;quot;] = GISDBASE&lt;br /&gt;
    env[&amp;quot;LOCATION_NAME&amp;quot;] = LOCATION_NAME&lt;br /&gt;
    env[&amp;quot;MAPSET&amp;quot;] = MAPSET&lt;br /&gt;
    return env&lt;br /&gt;
&lt;br /&gt;
def grass_exec(args):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    FIXED: list-based wrapper that preserves argument order exactly.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    env = grass_env()&lt;br /&gt;
    cmd = [GRASS_BIN, &amp;quot;--exec&amp;quot;] + args&lt;br /&gt;
    print(&amp;quot;&amp;gt;&amp;gt;&amp;quot;, &amp;quot; &amp;quot;.join(cmd))&lt;br /&gt;
    subprocess.run(cmd, check=True, env=env)&lt;br /&gt;
&lt;br /&gt;
def grass_r_neighbors(input_raster, output_raster,&lt;br /&gt;
                      method=&amp;quot;median&amp;quot;, size=7):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    Import GeoTIFF -&amp;gt; set region -&amp;gt; r.neighbors -&amp;gt; export GeoTIFF.&lt;br /&gt;
    Includes -f to allow DCELL-&amp;gt;Byte export.&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    in_name = &amp;quot;in_rast&amp;quot;&lt;br /&gt;
    out_name = &amp;quot;out_rast&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    # 1) import&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.in.gdal&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={input_raster}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={in_name}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    # 2) set region to raster (fixes extent issues)&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;g.region&amp;quot;,&lt;br /&gt;
        f&amp;quot;raster={in_name}&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    # 3) neighbors&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.neighbors&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={in_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;method={method}&amp;quot;,&lt;br /&gt;
        f&amp;quot;size={size}&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
    # 4) export (with -f)&lt;br /&gt;
    grass_exec([&lt;br /&gt;
        &amp;quot;r.out.gdal&amp;quot;,&lt;br /&gt;
        &amp;quot;-f&amp;quot;,&lt;br /&gt;
        f&amp;quot;input={out_name}&amp;quot;,&lt;br /&gt;
        f&amp;quot;output={output_raster}&amp;quot;,&lt;br /&gt;
        &amp;quot;format=GTiff&amp;quot;,&lt;br /&gt;
        &amp;quot;type=Byte&amp;quot;,&lt;br /&gt;
        &amp;quot;--overwrite&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
&lt;br /&gt;
def p(*parts):&lt;br /&gt;
    return str(BASE_PATH.joinpath(STATE, *parts))&lt;br /&gt;
&lt;br /&gt;
# ----------------- STEPS -----------------&lt;br /&gt;
&lt;br /&gt;
def step1_tree_canopy_reclass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Tree_Canopy_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + (A &amp;lt;= 0) * A&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 1: Trees-Combined&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step2_warp_tree_canopy_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Trees-Combined.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 2: Tree_Canopy_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step3_warp_land_cover_4326():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, &amp;quot;source&amp;quot;, f&amp;quot;NLCD_{YEAR}_Land_Cover_{STATE}{PART}.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    gdalwarp(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 3: Land_Cover_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step4_combine_tree_land_4326():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Canopy_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Land_Cover_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + &amp;quot;&lt;br /&gt;
        &amp;quot;((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 4: Tree_Land_Combined_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step5_replace_urban_clutter_with_grass():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 0) * 44 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 11) * 41 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 12) * 34 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 21) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 31) * 27 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 41) * 23 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 42) * 24 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 43) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 51) * 30 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 52) * 29 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 71) * 26 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 72) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 73) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 74) * 31 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 75) * 32 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 81) * 18 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 82) * 19 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 90) * 25 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 95) * 35&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 5: Grass-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step6_reclass_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Tree_Land_Combined_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = (&lt;br /&gt;
        &amp;quot;(A == 21)*10 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 22)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 23)*1 + &amp;quot;&lt;br /&gt;
        &amp;quot;(A == 24)*2&amp;quot;&lt;br /&gt;
    )&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, src,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 6: Reclassed-Urban_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step7_remove_clutter_roads_from_urban():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Reclassed-Urban_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=7)&lt;br /&gt;
    print(&amp;quot;Step 7: Urban-Only_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step8_combine_grass_and_clean_urban():&lt;br /&gt;
    a = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Urban-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    b = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Grass-Only_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    expr = &amp;quot;((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + ((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B&amp;quot;&lt;br /&gt;
    gdal_calc([&lt;br /&gt;
        &amp;quot;-A&amp;quot;, a,&lt;br /&gt;
        &amp;quot;-B&amp;quot;, b,&lt;br /&gt;
        &amp;quot;--outfile&amp;quot;, dst,&lt;br /&gt;
        &amp;quot;--calc&amp;quot;, expr,&lt;br /&gt;
        &amp;quot;--NoDataValue&amp;quot;, &amp;quot;0&amp;quot;,&lt;br /&gt;
        &amp;quot;--type&amp;quot;, &amp;quot;Byte&amp;quot;&lt;br /&gt;
    ])&lt;br /&gt;
    print(&amp;quot;Step 8: Combined-Clean_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step9_upsample_to_hd():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    ds = gdal.Open(src)&lt;br /&gt;
    if ds is None:&lt;br /&gt;
        raise RuntimeError(f&amp;quot;Cannot open {src}&amp;quot;)&lt;br /&gt;
    gt = ds.GetGeoTransform()&lt;br /&gt;
    original_xRes = gt[1]&lt;br /&gt;
    original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
    new_xRes = original_xRes / PERCENTAGE&lt;br /&gt;
    new_yRes = original_yRes / PERCENTAGE&lt;br /&gt;
&lt;br /&gt;
    gdal.Warp(&lt;br /&gt;
        dst,&lt;br /&gt;
        src,&lt;br /&gt;
        xRes=new_xRes,&lt;br /&gt;
        yRes=new_yRes,&lt;br /&gt;
        outputType=gdal.GDT_Byte&lt;br /&gt;
    )&lt;br /&gt;
    print(&amp;quot;Step 9: Combined-Clean-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step10_smooth_all_features():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Combined-Clean-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    grass_r_neighbors(src, dst, method=&amp;quot;median&amp;quot;, size=SIZE)&lt;br /&gt;
    print(&amp;quot;Step 10: Smoothed-HD_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def step11_convert_to_8bit_compressed():&lt;br /&gt;
    src = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD_4326.tiff&amp;quot;)&lt;br /&gt;
    dst = p(&amp;quot;data&amp;quot;, f&amp;quot;NLCD_{YEAR}_{STATE}{PART}_Smoothed-HD-Compressed_4326.tiff&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    gdal_translate(src, dst)&lt;br /&gt;
    print(&amp;quot;Step 11: Smoothed-HD-Compressed_4326&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ----------------- MAIN -----------------&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    step1_tree_canopy_reclass()&lt;br /&gt;
    step2_warp_tree_canopy_4326()&lt;br /&gt;
    step3_warp_land_cover_4326()&lt;br /&gt;
    step4_combine_tree_land_4326()&lt;br /&gt;
    step5_replace_urban_clutter_with_grass()&lt;br /&gt;
    step6_reclass_urban()&lt;br /&gt;
    step7_remove_clutter_roads_from_urban()&lt;br /&gt;
    step8_combine_grass_and_clean_urban()&lt;br /&gt;
    step9_upsample_to_hd()&lt;br /&gt;
    step10_smooth_all_features()&lt;br /&gt;
    step11_convert_to_8bit_compressed()&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144468</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144468"/>
		<updated>2026-05-15T16:49:50Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Running the docker container */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the Windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can add the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note: the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
--coastline requires OSM data mask from here [https://osmdata.openstreetmap.de/data/land-polygons.html OSM Shapefiles] Make sure you download the shapefiles on this page and not the alternate &amp;quot;Water polygons or Coastlines&amp;quot;. It is the land cover shapefiles that are used as the cutting mask, not the water line data.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144467</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144467"/>
		<updated>2026-05-15T16:32:19Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Building the terrain */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can add the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note: the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
--coastline requires OSM data mask from here [https://osmdata.openstreetmap.de/data/land-polygons.html OSM Shapefiles] Make sure you download the shapefiles on this page and not the alternate &amp;quot;Water polygons or Coastlines&amp;quot;. It is the land cover shapefiles that are used as the cutting mask, not the water line data.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144466</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144466"/>
		<updated>2026-05-15T16:23:46Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Generating Terrain */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can add the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note: the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=World_Scenery_3.0_coastlines&amp;diff=144465</id>
		<title>World Scenery 3.0 coastlines</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=World_Scenery_3.0_coastlines&amp;diff=144465"/>
		<updated>2026-05-15T16:22:44Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;'''NOTE: genVPB.py now includes a flag to process the coastline, --coastline [path_to_coastline_shapefile]'''&lt;br /&gt;
&lt;br /&gt;
{{WS30 Navbar}}&lt;br /&gt;
World Scenery 3.0 uses a texture of landclass information, typically with 10-25m resolution.  For coastlines in particular, this creates an obvious sharp edge with right angles.&lt;br /&gt;
[[File:Fgfs-20220208201349.jpg|thumb|WS3.0 coastline just using a 10m raster with no vector data, showing the coastline problem]]&lt;br /&gt;
[[File:Fgfs-20230219173559.jpg|thumb|WS3.0 coastline using OSM vector data.  Note the artifacts where the data doesn't line up.]]&lt;br /&gt;
[[File:Fgfs-20230227220551.jpg|alt=|thumb|Current state-of-the-art coast rendering, using OSM data for both raster and coastline definition.  Martinique]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
This page describes various techniques/experiments to find a better solution.&lt;br /&gt;
&lt;br /&gt;
=== Current Rendering ===&lt;br /&gt;
The current &amp;quot;state of the art&amp;quot; is to is to create a second texture at much higher resolution and paint the coastline onto this.  The fragment shader then mixes the standard resolution landclass and the much higher resolution coastline texture.  This has the advantage of being able to mix the data together nicely.    &lt;br /&gt;
&lt;br /&gt;
We currently support both generating this higher resolution raster at scenery generation time and also at runtime.  Both the generation of the texture and the shader are currently quite basic and would benefit from further development from anyone interested in this area.&lt;br /&gt;
&lt;br /&gt;
The significant downside is the increased memory occupancy of the additional texture and the added complexity of the shader.&lt;br /&gt;
&lt;br /&gt;
This still leaves an issue that the elevation mesh is completely regular, so where steep slopes meet the sea, the high water mark can be a half way up a slope!  The shader creates cliffs and beaches instead of water depending on the steepness of the elevation mesh.  We also force any vertex that is in the sea to 0 elevation.&lt;br /&gt;
&lt;br /&gt;
=== OpenStreetMap or other vector coastline data ===&lt;br /&gt;
OpenStreetMap has line data for coastlines and water bodies.  This is vector data, so can be rendered at higher apparent resolution than the landclass data.  The gencoastline.py script in fgmeta/ws30 uses this to generate a series of lat/lon points that FlightGear can then use.&lt;br /&gt;
&lt;br /&gt;
There is a problem where the OSM coastline (defined as the mean high water mark) doesn't exactly match the landclass raster due to projection discrepancies or simply because of state of the tide at the time that a satellite image was taken.    &lt;br /&gt;
&lt;br /&gt;
Ideally the coastline of the raster will match the OSM coastline as closely as possible, and perhaps overlap slightly.  &lt;br /&gt;
&lt;br /&gt;
To do this:  &lt;br /&gt;
&lt;br /&gt;
# Reclassify the Ocean as NODATA (0.0)&lt;br /&gt;
#* r.reclass using https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/remove_sea.txt.  This reclassifies 44 (Sea) as NODATA&lt;br /&gt;
&lt;br /&gt;
# Use the GDAL Fill nodata to fill out the edges for radius 10.  You will now have a raster as if the sea level has dropped.  Use one of the following&lt;br /&gt;
#*&amp;lt;code&amp;gt;gdal_fillnodata.py -md 10 -b 1 -of GTiff &amp;lt;original_file&amp;gt; &amp;lt;new_file&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
#* r.fill.stats using the mode as the statistical method&lt;br /&gt;
# Get a shapefile of the coastline from OSM.  The simplest way is to download the WGS84 split polygons from here: https://osmdata.openstreetmap.de/data/land-polygons.html.  You may wish to pre-process these to clip to the rough extents of your scenery to reduce the size of the file.&lt;br /&gt;
&lt;br /&gt;
# Load the OSM shapefile and Use Clip raster by mask layer this will cut out the landmass much more accurately than before.&lt;br /&gt;
#* gdalwarp -overwrite -of GTiff -tr 0.00015600000000000002 -9.000000000000003e-05 -tap -cutline &amp;lt;vector.osm&amp;gt; -cl multipolygons &amp;lt;input&amp;gt; &amp;lt;output&amp;gt;&lt;br /&gt;
# Use the GDAL Fill nodata to fill out the edges for radius 1.  This ensures that there is &amp;quot;land&amp;quot; underneath the shoreline itself.&lt;br /&gt;
#* gdal_fillnodata.py -md 1 -b 1 -of GTiff &amp;lt;input&amp;gt; &amp;lt;output&amp;gt;&lt;br /&gt;
# Use the standard Fill NoData cells to fill the NoData with the correct Ocean landclass.&lt;br /&gt;
#* gdal_calc.py -A &amp;lt;input&amp;gt; --NoDataValue=44 --calc=&amp;quot;A&amp;quot; --outfile &amp;lt;output&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The genVPB.py script performs these steps.&lt;br /&gt;
&lt;br /&gt;
=== Experiments ===&lt;br /&gt;
&lt;br /&gt;
==== Direct model generation ====&lt;br /&gt;
Initial experiments using the same approach as roads (overlaying an additional tri-strip for the coastline) did not blend well with the underlying landclass mesh.&lt;br /&gt;
&lt;br /&gt;
'''Scenery-time generation of detailed coastline raster'''&lt;br /&gt;
&lt;br /&gt;
fgmeta/ws30/gencoastlineraster.py generates a raster file that can be loaded by FG at runtime.  This is quite effective and leverages python raster packages.  The downside is the additional disk space.&lt;br /&gt;
&lt;br /&gt;
=== Thought Experiments ===&lt;br /&gt;
&lt;br /&gt;
==== Nearest Neighbour Encoding ====&lt;br /&gt;
The landclass texture currently has unused channels.  It would be possible to use one of those channels to encode information about the neighbouring texels.  This could be used for mixing coastline fragments better.  For example, encoding whether the 8 surrounding texels are water or land could be done in 8 bits, conveniently 256 different values.  A separate texture could be conceived that defined a different shape of coastline for each of the 256 combinations, probably with some perturbations to avoid obvious repetition.&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144464</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144464"/>
		<updated>2026-05-15T16:14:45Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Running the docker container */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can add the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144463</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144463"/>
		<updated>2026-05-15T16:14:15Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Running the docker container */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
In the windows environment using Docker Desktop, if you need a path to a source of elevation data and a path to the OSM shoreline shapefiles, you can od the following:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/vector/land-polygons-complete-4326,target=/home/flightgear/coastlines&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=G:/Scenery/ws3.0/Alabama/output/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
Note the  --mount &amp;quot;type=bind,source=G:/Scenery/WS3.0-extra/SRTM-1,target=/home/flightgear/SRTM-3,readonly&amp;quot;, the genVPB.py script in the Docker container is expecting a location of  /home/flightgear/SRTM-3 for the elevation data. In this example I am using SRTM-1 elevation data, but we still map it to the expected  /home/flightgear/SRTM-3 Docker container location.&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144462</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144462"/>
		<updated>2026-05-15T16:04:49Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Building the terrain */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp&lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144461</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144461"/>
		<updated>2026-05-15T16:03:19Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Building the terrain */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
&lt;br /&gt;
./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp &lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
&lt;br /&gt;
./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144460</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144460"/>
		<updated>2026-05-15T16:02:11Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Building the terrain */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska:&lt;br /&gt;
./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp &lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features):&lt;br /&gt;
./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144459</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144459"/>
		<updated>2026-05-15T16:01:35Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Building the terrain */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
Another example, to generate a coastal area with shorelines and lakes, like Alaska&lt;br /&gt;
./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp &lt;br /&gt;
&lt;br /&gt;
Same thing with roads and rails (line features)&lt;br /&gt;
./scripts/genVPB.py --bbox 68 -167 69 -152 --raster ./data/NLCD_2016_Alaska167-153_68_Smoothed-HD-Compressed_4326.tiff --hgt-dir ./SRTM-3/ --generate-water-raster --shrink-water 4 --coastline ./coastlines/land_polygons.shp  --generate-line-features&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144458</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144458"/>
		<updated>2026-05-15T15:56:50Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Adding water */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPB.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144457</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144457"/>
		<updated>2026-05-15T15:56:30Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Adding water */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use genVPG.py ...... --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask]'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144456</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144456"/>
		<updated>2026-05-15T15:55:46Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Adding water */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
'''Use --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144455</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144455"/>
		<updated>2026-05-15T15:55:30Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Adding water */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
'''Use --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144454</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144454"/>
		<updated>2026-05-15T15:55:13Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Adding water */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.&lt;br /&gt;
use --generate-water-raster --shrink-water 4 --coastline [path_to_shapefile_mask'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144453</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144453"/>
		<updated>2026-05-15T15:53:44Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Adding water */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144452</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144452"/>
		<updated>2026-05-15T15:52:58Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Adding water */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
'''Update 05/12/2026:  Depreciated, it is now built into genVPB.py above.'''&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144451</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144451"/>
		<updated>2026-05-15T15:46:53Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Python script to process Sentinel-2 data */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
This is a slightly different process that that of the NLCD. Sentinel-2 data does not have any extra tree canopy coverage to merge in but this script still cuts out some superfluous urban clutter. Sentinel-2 data uses a two stage workflow. Step 1 cleans up the urban clutter and then splits the larger Sentinel-2 chunks of date to 1x1 tiles to help facilitate Step 2, which up-samples and smoothes the boundaries.&lt;br /&gt;
&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 7)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
This is the pretty much the same final steps that the NLCD uses to up-sample and smooth the boundaries.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
#ratio of 11/15 upscale to smoothing is the next sweet spot, but starts to move boundary positions and starts to eliminate smaller land class features for not a ton of resolution gain.&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144450</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144450"/>
		<updated>2026-05-15T15:45:23Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
This is a slightly different process that that of the NLCD. Sentinel-2 data does not have any extra tree canopy coverage to merge in but this script still cuts out some superfluous urban clutter. It uses a two stage workflow. Step 1 cleans up the urban clutter and then splits the larger Sentinel-2 chunks of date to 1x1 tiles to help facilitate Step 2 processing wich up-samples and smoothes the boundaries.&lt;br /&gt;
&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 7)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
This is the pretty much the same final steps that the NLCD uses to up-sample and smooth the boundaries.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
#ratio of 11/15 upscale to smoothing is the next sweet spot, but starts to move boundary positions and starts to eliminate smaller land class features for not a ton of resolution gain.&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144449</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144449"/>
		<updated>2026-05-15T15:39:38Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 1 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
This is a slightly different process that that of the NLCD. Sentinel-2 data does not have any extra tree canopy coverage to merge in but this script still cuts out some superfluous urban clutter. It uses a two stage workflow. Step 1 cleans up the urban clutter and then splits the larger Sentinel-2 chunks of date to 1x1 tiles to help facilitate Step 2 processing wich up-samples and smoothes the boundaries.&lt;br /&gt;
&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 7)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144448</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144448"/>
		<updated>2026-05-15T15:32:13Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Optional Python script to process NLCD for the USA */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Optional Python script to process NLCD for the USA ===&lt;br /&gt;
You can optionally process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
NOTE: the current genVPB.py script (osgdem) will not generate the final scenery water/land edge to this high of a resolution. It is set to a default tile size of 256x256, &amp;quot;--tile-image-size 256&amp;quot;. This resolution is much higher than that. To achieve the same base resolution in the built scenery, osgdem needs a switch to &amp;quot;--tile-image-size 1024&amp;quot; to realize the higher resolution. However, when you apply the water mask flags --generate-water-raster --shrink-water 4 --coastline [path_to_coastal_shapefiles] using genVPB.py the cut mask will achieve the higher resolution. &lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144311</id>
		<title>Howto:Create WS3.0 terrain</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Howto:Create_WS3.0_terrain&amp;diff=144311"/>
		<updated>2026-05-06T15:27:30Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Python_script_to_process_Sentinel-2_data */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{WS30 Navbar}}&lt;br /&gt;
This article provides instructions on how to generate basic WS3.0 terrain.&lt;br /&gt;
&lt;br /&gt;
WS3.0 terrain consists of three parts:&lt;br /&gt;
&lt;br /&gt;
# A terrain mesh consisting of a landclass texture draped over an elevation model.  &lt;br /&gt;
# A high resolution water raster used to show water features such as rivers, lakes and coastline with more definition&lt;br /&gt;
# Line features such as roads and railways.&lt;br /&gt;
&lt;br /&gt;
The terrain is generated by a set of tools that are packaged in a docker image for convenience.[[File:Diagram-export-21-12-2023-16 29 37.png|thumb|Basic WS3.0 Scenery Generation Process]]&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a Workspace ===&lt;br /&gt;
Create a directory with the following sub-directories:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/vpb&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;output/Terrain&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Docker ===&lt;br /&gt;
&lt;br /&gt;
# Install [https://docs.docker.com/get-started/ Docker] on your platform.&lt;br /&gt;
#Pull the docker image by running the following command&lt;br /&gt;
&lt;br /&gt;
 docker pull flightgear/ws30-vpb-generator:latest&lt;br /&gt;
Optionally, if you are hitting rate limits:&lt;br /&gt;
#Create an account on https://hub.docker.com/.  (Note that you will need to click on an email verification link before you can log in for the first time)&lt;br /&gt;
#Run &amp;lt;code&amp;gt;docker login&amp;lt;/code&amp;gt; before the '''docker pull''' command above&lt;br /&gt;
&lt;br /&gt;
== Getting the base data ==&lt;br /&gt;
You need two pieces of data for the area of scenery you are generating:&lt;br /&gt;
&lt;br /&gt;
# An elevation model (aka DEM).  This indicates what altitude each point of the surface is.&lt;br /&gt;
# Landclass data showing what type of terrain is at each point of the surface.  This is often either a Raster (effectively a texture), or vector data.  &lt;br /&gt;
&lt;br /&gt;
=== Elevation Model ===&lt;br /&gt;
Download the NASADEM elevation model for the area of scenery you wish to generate.  This is available in 1x1 degree blocks from [https://lpdaac.usgs.gov/products/nasadem_hgtv001/ here], and with an interactive browser [https://search.earthdata.nasa.gov/search here].  &lt;br /&gt;
&lt;br /&gt;
Unzip the files into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
=== Landclass Raster ===&lt;br /&gt;
Download an landclass raster for the area of scenery you wish to generate.&lt;br /&gt;
&lt;br /&gt;
* For Europe, use of [https://land.copernicus.eu/pan-european/corine-land-cover/clc2018 CORINE] is recommended.&lt;br /&gt;
* For the USA [https://www.mrlc.gov/viewer/ NLCD] is recommended&lt;br /&gt;
* Sentinel-2 data is available for the entire world via [https://livingatlas.arcgis.com/landcoverexplorer/ ESRI], but has limited set of landclasses.&lt;br /&gt;
&lt;br /&gt;
Put these into the &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
More detailed terrain can be created by modifying the landclass raster, and/or generating a new raster from vector data.  These processes are discussed below.&lt;br /&gt;
&lt;br /&gt;
== Generating Terrain ==&lt;br /&gt;
To generate terrain you need to run the tools within the docker container we installed above.  The docker image is like a small, independent virtual computing environment running within your system.  This particular docker image has all the scenery generation tools already installed.&lt;br /&gt;
&lt;br /&gt;
=== Running the docker container ===&lt;br /&gt;
Firstly, get the container running from the directory containing your &amp;lt;code&amp;gt;cache&amp;lt;/code&amp;gt;,  &amp;lt;code&amp;gt;data&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;output&amp;lt;/code&amp;gt; directories:&lt;br /&gt;
 docker run --rm --mount &amp;quot;type=bind,source=`pwd`/data,target=/home/flightgear/data,readonly&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/output,target=/home/flightgear/output&amp;quot; --mount &amp;quot;type=bind,source=`pwd`/cache,target=/home/flightgear/cache&amp;quot; -it flightgear/ws30-vpb-generator:latest /bin/bash&lt;br /&gt;
&lt;br /&gt;
You should now find yourself in a bash shell within your container.  You should see data and output directories which are linked to the directories you created earlier:&lt;br /&gt;
 flightgear@ddcac77f7d5e:~$ ls&lt;br /&gt;
 cache data output bin scripts&lt;br /&gt;
&lt;br /&gt;
=== Building the terrain ===&lt;br /&gt;
To build the terrain mesh, use the &amp;lt;code&amp;gt;genVPB.py&amp;lt;/code&amp;gt; tool from inside the docker container:&lt;br /&gt;
&lt;br /&gt;
 Usage: genVPB.py --raster &amp;lt;input-raster&amp;gt; [ option ... ]&lt;br /&gt;
 Usage: genVPB.py --bbox &amp;lt;lat0&amp;gt; &amp;lt;lon0&amp;gt; &amp;lt;lat1&amp;gt; &amp;lt;lon1&amp;gt; --sentinel --reclass &amp;lt;reclass&amp;gt; [ option ... ]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --raster RASTER                      Input landclass raster.&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1           Bounding box of scenery to be generated&lt;br /&gt;
 --sentinel                           Use Sentinel2 landclass tiles&lt;br /&gt;
 --sentinel-dir SENTINEL_DIR          Directory for Sentinel Data. Default /home/flightgear/data/Sentinel-2&lt;br /&gt;
 --hgt-dir HGT_DIR                    Directory containing HGT DEM files. Default /home/flightgear/data/NASADEM&lt;br /&gt;
 --output-dir OUTPUT_DIR              Set output directory. Default /home/flightgear/output&lt;br /&gt;
 --download-sentinel                  Download Sentinel2 tiles if needed&lt;br /&gt;
 --reclass RECLASS                    Reclassify raster using file &amp;lt;reclass&amp;gt;. See ./scripts/mappings/&lt;br /&gt;
 --coastline COASTLINE                Clip against coastline against polygon (.osm)&lt;br /&gt;
 --shrink-water SHRINK_WATER          Shrink water bodies (landclasses 40, 41) by &amp;lt;pixels&amp;gt; pixels&lt;br /&gt;
 --generate-water-raster              Generate a water raster from OSM data&lt;br /&gt;
 --generate-line-features             Generate a water raster from OSM data&lt;br /&gt;
 --cache-dir CACHE_DIR                Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --nasadem-server NASADEM_SERVER      Set server to download NASADEM data from. Default https://e4ftl01.cr.usgs.gov/MEASURES/NASADEM_HGT.001/2000.02.11/)&lt;br /&gt;
 --nasadem-user NASADEM_USER          NASA Earthdata username.&lt;br /&gt;
 --nasadem-password NASADEM_PASSWORD  NASA Earthdata password.&lt;br /&gt;
 --debug                              Debug output&lt;br /&gt;
&lt;br /&gt;
For example, to generate a piece of terrain around Edinburgh (latitude 55.5, longitude 3 degrees West):&lt;br /&gt;
 ./scripts/genVPB.py --bbox 55 -4 56 -3 --raster ./data/uk_wgs84_10m_N54.tif&lt;br /&gt;
&lt;br /&gt;
If you are using anything other than a CORINE raster you will need to reclassify the data to match the landclasses used by FlightGear.  Those classes are defined in [https://gitlab.com/flightgear/fgdata/-/tree/next/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].  You can reclassify them using the files in the [https://gitlab.com/flightgear/fgmeta/-/tree/next/ws30/mappings/ scripts/mappings] directory. E.g. to reclassify NLCD2019 data you can use &amp;lt;code&amp;gt;--reclassify ./scripts/mappings/nlcd2019.txt&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
genVPB.py will output the data to the output/vpb directory, in which you should find a series of files and directories.&lt;br /&gt;
&lt;br /&gt;
=== Adding water ===&lt;br /&gt;
The terrain mesh does not have highly detailed water features - as typically the source data has a resolution of 10-25m.  Water features are generated from OpenStreetMap data.  To generate water features simply run the &amp;lt;code&amp;gt;genwaterraster.py&amp;lt;/code&amp;gt; command.&lt;br /&gt;
&lt;br /&gt;
 Usage: genwaterraster.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--output-dir OUTPUT_DIR] [--cache-dir CACHE_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1    Bounding box of scenery to be generated&lt;br /&gt;
 --debug                       Debug output&lt;br /&gt;
 --output-dir OUTPUT_DIR       Directory to write files into. Default /home/flightgear/output&lt;br /&gt;
 --cache-dir CACHE_DIR         Directory for OSM data cache. Default /home/flightgear/cache&lt;br /&gt;
&lt;br /&gt;
For example, to generate water for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genwaterraster.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/vpb directory there should be a set of directories and .png files.&lt;br /&gt;
&lt;br /&gt;
=== Adding roads and railways ===&lt;br /&gt;
The terrain mesh does not have any line features - things like roads.  These are generated separately from OpenStreetMap data.  To generate line features simply run the &amp;lt;code&amp;gt;genroads.py&amp;lt;/code&amp;gt; command:&lt;br /&gt;
&lt;br /&gt;
 Usage: ./scripts/genroads.py [-h] --bbox lat0 lon0 lat1 lon1 [--debug] [--threads THREADS] [--cache-dir CACHE_DIR] [--output-dir OUTPUT_DIR]&lt;br /&gt;
&lt;br /&gt;
 Arguments:&lt;br /&gt;
 --bbox lat0 lon0 lat1 lon1     Bounding box of scenery to be generated&lt;br /&gt;
 --debug                        Debug output&lt;br /&gt;
 --threads THREADS              Number of parallel threads to run. Defaults to 1&lt;br /&gt;
 --cache-dir CACHE_DIR          Directory for OSM data cache. Default /home/flightgear/cache/&lt;br /&gt;
 --output-dir OUTPUT_DIR        Set output directory. Default /home/flightgear/cache/&lt;br /&gt;
&lt;br /&gt;
For example, to generate roads for the area around Edinburgh:&lt;br /&gt;
 ./scripts/genroads.py --bbox 55 -4 56 -3&lt;br /&gt;
&lt;br /&gt;
Inside the ./output/Terrain directory there should be a set of directories and, .STG files text files.&lt;br /&gt;
&lt;br /&gt;
==Running FlightGear with the new WS3.0 Terrain==&lt;br /&gt;
To test the new terrain, simply include the output directory in your scenery path and run FlightGear with the &amp;lt;code&amp;gt;--prop:/scenery/use-vpb=true&amp;lt;/code&amp;gt; to enable WS3.0.&lt;br /&gt;
&lt;br /&gt;
== Advanced Techniques ==&lt;br /&gt;
The following sections describe more complex techniques to generate higher quality WS3.0 terrain.  Almost all of them involve using different data sources to generate a more detailed landclass raster before running the final scenery generation processes described above.  Generating a highly detailed landclass raster is where the magic happens.  &lt;br /&gt;
&lt;br /&gt;
Most techniques use gdal or grass to modify the raster/vector data, typically using the QGIS program.&lt;br /&gt;
&lt;br /&gt;
=== Using a different elevation model ===&lt;br /&gt;
If you are using another elevation model other than NASAEM, then you may need to re-project it using QGIS/gdalwarp to the WGS84 CRS (aka EPSG:4326).  &lt;br /&gt;
&lt;br /&gt;
=== Landclass Data Requirements ===&lt;br /&gt;
For any landclass data we need to ensure the data is in the correct format.  That means:&lt;br /&gt;
&lt;br /&gt;
# Is a Raster (geotiff) rather than Vector data.  This raster will become the texture on the terrain that the terrain shaders do their magic on.&lt;br /&gt;
# Uses the WGS84 Coordinate Reference System.  The ensures that the terrain generation step is efficient.&lt;br /&gt;
# Has the correct landclass values for each terrain type.  We use a set of values based on the CORINE raster set, defined in [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml].&lt;br /&gt;
&lt;br /&gt;
Below is a quick table showing what steps you need to take for common landclass data sources.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
!Landclass Data&lt;br /&gt;
!Warp to WGS84 required?&lt;br /&gt;
!Landclass re-classification Required?&lt;br /&gt;
!Raster Simplification Required?&lt;br /&gt;
!Conversion to Raster Required?&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Raster&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|CORINE Vector&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|-&lt;br /&gt;
|NLCD&lt;br /&gt;
|No&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|-&lt;br /&gt;
|Sentinel-2&lt;br /&gt;
|Yes&lt;br /&gt;
|Yes&lt;br /&gt;
|No&lt;br /&gt;
|No&lt;br /&gt;
|}&lt;br /&gt;
Conversion to Raster must be done manually.  Converting to WGS84 and the correct landclasses ''can'' be done by the genVPB.py script, but slows down scenery generation.  Therefore if you are planning to generate scenery multiple times it is best to pre-process the files yourself.&lt;br /&gt;
&lt;br /&gt;
The easiest way to do these operations is using QGIS, which is available for most platforms.  If you are scripting a toolchain, the QGIS tools include command-line equivalents for all commands.&lt;br /&gt;
&lt;br /&gt;
When using QGIS, set the Project CRS to WGS84 (aka EPSG:4326).  You can then add layers of Raster or Vector data from files from the &amp;lt;code&amp;gt;Layer-&amp;gt;Add Layer&amp;lt;/code&amp;gt; menu.  When performing any operations, &amp;lt;u&amp;gt;always&amp;lt;/u&amp;gt; write out the data to a real file so you can go back to it later. Disk space is cheap :).&lt;br /&gt;
&lt;br /&gt;
=== Warping Raster Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.&lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a raster layer to a different CRS using the Raster-&amp;gt;Projections-Warp (Reproject) tool.  &lt;br /&gt;
&lt;br /&gt;
Select the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Resampling Method to Use - Nearest Neighbour.  (Landclass data is not like normal images.  You don't want to interpolate between values.)&lt;br /&gt;
* Nodata value for output bands - 0.0  (This means that any data at the edges will be Ocean, usually a reasonable default)&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
Alternatively you can do this step from the commandline.&lt;br /&gt;
 gdalwarp -t_srs EPSG:4326 -dstnodata 0.0 -r near -ot Byte -of GTiff -co COMPRESS=NONE -co BIGTIFF=IF_NEEDED /home/stuart/FlightGear/VPB/data/CORINE/u2018_clc2018_v2020_20u1_raster100m/DATA/U2018_CLC2018_V2020_20u1.tif /home/stuart/FlightGear/VPB/data/scratch/corine_WGS84.tif&lt;br /&gt;
&lt;br /&gt;
=== Warping Vector Layers to WGS84 ===&lt;br /&gt;
genVPB.py will automatically convert to WGS84 if it detects a landclass raster with a different Coordinate System.  However, you may wish to do so manually instead so all your data is on the same coordinate system.  &lt;br /&gt;
&lt;br /&gt;
In QGIS you can warp a vector layer using Vector-&amp;gt;Data Management Tools-&amp;gt;Reproject Layer.  &lt;br /&gt;
&lt;br /&gt;
Set the following options in the dialog:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - Check you have selected the correct layer. The CRS is shown at the right.&lt;br /&gt;
* Target CRS -   set to &amp;lt;code&amp;gt;EPSG:4326 - WGS84&amp;lt;/code&amp;gt;, which should be your project CRS.&lt;br /&gt;
* Reprojected - Choose a target filename&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying Vector Layers ===&lt;br /&gt;
For CORINE vector data in particular, the attributes used in the vector data are not the same as those used by the CORINE Raster data.  So we need to create a new attribute on the data.&lt;br /&gt;
[[File:Field Calculator.png|thumb|QGIS Field Calculator]]&lt;br /&gt;
To do this &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;.  You should see a table with multiple columns.  Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialog&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;quot;Whole Number (Integer)&amp;quot;.  This will create a new column which we will populate with the correct landclass data&lt;br /&gt;
* Click on the &amp;lt;code&amp;gt;Open Field Calculator&amp;lt;/code&amp;gt; button (Ctrl + I).  (If you get an error about only being able to create Virtual fields, go back to the Layer menu, export it and open the exported file).&lt;br /&gt;
* Select the following options:&lt;br /&gt;
** Update Existing Field&lt;br /&gt;
** Select the Landclass field you just created.&lt;br /&gt;
** Copy the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/corine_vector.txt into the Expression box (without the comment lines starting with &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;).  This is just some simple code to set the attribute correctly.  The code should be correct for CORINE vector data.  If your data is from other sources you will need to work out how you want to map your source data landclasses to the CORINE ones.  [https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Materials/base/landclass-mapping.xml Materials/base/landclass-mapping.xml] can be used as a guide.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;OK&amp;lt;/code&amp;gt;.  You should see that your landclass column is now populated with the landclass data.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save you changes&lt;br /&gt;
&lt;br /&gt;
=== Creating a Raster from a Vector Layer ===&lt;br /&gt;
To create a Raster from a Vector Layer select &amp;lt;code&amp;gt;Raster-&amp;gt;Conversion-&amp;gt;Rasterize (Vector to Raster)&amp;lt;/code&amp;gt;.  &lt;br /&gt;
[[File:QGIS Rasterize (Vector to Raster).png|thumb|Creating a Raster from a Vector Layer - QGIS Rasterize]]&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Field to use for burn-in value - select the &amp;lt;code&amp;gt;Landclass&amp;lt;/code&amp;gt; column you created above.&lt;br /&gt;
* Output raster size units.  This is going to set the resolution of your raster.  You can work out the resolution in two different ways:&lt;br /&gt;
** Select &amp;quot;Georeferenced units&amp;quot; and determine how many degrees each pixel is in latitude and longitude.&lt;br /&gt;
** Select &amp;quot;Pixels&amp;quot; and determine the size of raster you want in pixels.  [https://www.nhc.noaa.gov/gccalc.shtml This] is a good calculator to help. You input e.g. SE and SW coordinates and calculate to get the distance in Km. Then you multiply by thousand and devide by the number of metres per pixel (e.g. 5) -&amp;gt; resolution for width.&lt;br /&gt;
* Width/Horizontal Resolution. Enter the values you've calculated for the horizontal resolution (longitudinal), or the width of the raster&lt;br /&gt;
* Height/Vertical Resolution. Enter the values you've calculated for the vertical resolution (latitude or the height of the raster)&lt;br /&gt;
* Output extent - Select an option from the box on the right. You can edit the text afterwards (NB: East, West, South, North). Best practise is to create long thin strips of 1 degree latitude in height, as this makes subsequent processing much easier.&lt;br /&gt;
* Assign a specific nodata value to output bands - Select 0.0 for Ocean.  CORINE vector data in particular has a lot of nodata for Oceans&lt;br /&gt;
* Advanced Parameters - No Compression&lt;br /&gt;
* Output data type - Byte&lt;br /&gt;
* Rasterized - Select a new filename&lt;br /&gt;
&lt;br /&gt;
=== Simplifying a Raster Layer ===&lt;br /&gt;
Some Raster Landclass data (NLCD included) has too much noise - in particular large US highway systems are identified as Urban areas.&lt;br /&gt;
&lt;br /&gt;
To smooth it out we can use the GRASS &amp;lt;code&amp;gt;n.neighbors&amp;lt;/code&amp;gt; function from the Processing Toolbox in QGIS.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Layer - correct layer, check CRS&lt;br /&gt;
* Neighborhood operation - median.  (This is not a normal image, so using an average will result in weird values)&lt;br /&gt;
* Neighborhood size - 5.&lt;br /&gt;
* Neighbors - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
=== Clipping a Raster Layer with OSM Data for Land (Corine) ===&lt;br /&gt;
The Corine dataset does not match OSM coastlines exactly. The following multi-stage process makes sure, that no Corine land-use is in the water as defined by OSM. &lt;br /&gt;
&lt;br /&gt;
==== Download OSM Land Data ====&lt;br /&gt;
&lt;br /&gt;
Download land polygons based on OSM data as a Shapefile from [https://osmdata.openstreetmap.de/data/land-polygons.html Land Polygons] and make sure to pick the WGS84 projected download with split polygons (&amp;quot;Large polygons are split, use for larger scales&amp;quot;). Once downloaded unzip the content into a directory.&lt;br /&gt;
&lt;br /&gt;
==== Reclassifying the OSM Land Data Vector Layer ====&lt;br /&gt;
I QGIS make sure that only the layer for the raster for land data is selected (e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;) -&amp;gt; in the map view you will see the whole earth. NB: typically you do this reclassify only once after download and can reuse the result for future processing.&lt;br /&gt;
&lt;br /&gt;
Then: &lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Open Attribute Table&amp;lt;/code&amp;gt;. You should see a table with multiple columns. Each row represents a feature in the data.&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;Toggle Editing Mode&amp;lt;/code&amp;gt; (Ctrl + E), on the top left of the dialogue&lt;br /&gt;
* Click on &amp;lt;code&amp;gt;New Field&amp;lt;/code&amp;gt; (Ctrl+W) and create a new field called Landclass of type &amp;lt;code&amp;gt;Integer (32 bit)&amp;lt;/code&amp;gt;. This will create a new column which we will populate with the correct land class data&lt;br /&gt;
* On top of the table on the left side choose &amp;quot;Landclass&amp;quot; in the drop-down menu, then input &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt; into the field to the right and then press button &amp;quot;Update&amp;quot; all to the left of this field.&lt;br /&gt;
* Wait a bit and the close the dialogue.&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Layer-&amp;gt;Save Layer Edits&amp;lt;/code&amp;gt; to save your changes (overwrite e.g. &amp;lt;code&amp;gt;land-polygons-split-4326&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Convert the Land Data from Vector to Raster ====&lt;br /&gt;
Do the same as in chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; above. The only difference is that the Input layer will be the land data polygons and you need to choose a different file name for the &amp;quot;Rasterized&amp;quot; (e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
&lt;br /&gt;
==== Remove Novalue Entries in the Land Data Raster ====&lt;br /&gt;
To do this:&lt;br /&gt;
&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Processing-&amp;gt;Toolbox&amp;lt;/code&amp;gt;. You should see a new box on the right side.&lt;br /&gt;
* Write &amp;quot;gdal_calc&amp;quot; in the search box and you should see an entry &amp;quot;Raster calculator&amp;quot;. Double click on it and you will get a new dialogue window.&lt;br /&gt;
* In this dialogue:&lt;br /&gt;
** For &amp;quot;Input layer A&amp;quot; choose the raster from the previous chapter ((e.g. osm_land_scotland_5m.tif)&lt;br /&gt;
** In field &amp;quot;Calculation in gdalnumeric ...&amp;quot; write: &amp;lt;code&amp;gt;greater(A,0) * A&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Output raster type&amp;quot; choose &amp;lt;code&amp;gt;Byte&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Advanced Parameters&amp;quot; choose Profile &amp;lt;code&amp;gt;No compression&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Additional command-line parameters&amp;quot; write: &amp;lt;code&amp;gt;--hideNoData&amp;lt;/code&amp;gt;&lt;br /&gt;
** In field &amp;quot;Calculated&amp;quot; choose a file (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
** (In the &amp;quot;GDAL/OGR console call&amp;quot; it will have something similar to the follwing - just with different paths: &amp;lt;code&amp;gt;gdal_calc.py --overwrite --calc &amp;quot;greater(A ,0) * A&amp;quot; --format GTiff --type Byte -A /home/vanosten/custom-fg-scenery/data/osm_land_scotland_5m.tif --A_band 1 --co COMPRESS=NONE --co BIGTIFF=IF_NEEDED --hideNoData --outfile /home/vanosten/custom-fg-scenery/data/osm_land_scotland_allvalues_5m.tif&amp;lt;/code&amp;gt;&lt;br /&gt;
** Press the &amp;quot;Run&amp;quot; button - and when complete close the dialogue.&lt;br /&gt;
&lt;br /&gt;
You should now see a map only black and white. You can check for correctness by pressing &amp;lt;code&amp;gt;CTRL+SHIFT+I&amp;lt;/code&amp;gt; to get a cursor with an arrow and an &amp;quot;i&amp;quot;. First make sure the new raster is selected on the left side. Next click on the sea/ocean and then check in the &amp;quot;Identify Results&amp;quot; window on the right that the value is &amp;lt; 2. The click on the land and check that the value is 2.&lt;br /&gt;
&lt;br /&gt;
==== Create the Final Clipped Corine Raster Against OSM Land Data =====&lt;br /&gt;
Do the following:&lt;br /&gt;
&lt;br /&gt;
* In QGIS make sure that you have only the following two layers: the basis Corine raster (see chapter &amp;quot;Creating a Raster from a Vector Layer&amp;quot; - here e.g. corine_raster_scotland_5m.tif) and plus the raster from the previous step (e.g. osm_land_scotland_allvalues_5m.tif)&lt;br /&gt;
* Select &amp;lt;code&amp;gt;Raster-&amp;gt;Raster Calculator ...&amp;lt;/code&amp;gt; and a corresponding dialogue will open showing on the left hand side the two rasters.&lt;br /&gt;
* Choose a new &amp;quot;Output layer&amp;quot; (e.g. corine_raster_scotland_clipped_5m.tif).&lt;br /&gt;
* In the &amp;quot;Raster Calculator Expression&amp;quot; field input: &amp;lt;code&amp;gt;if (&amp;quot;osm_land_scotland_all_data_5m@1&amp;quot; &amp;lt; 2, 44, &amp;quot;corine_raster_scotland_5m@1&amp;quot;)&amp;lt;/code&amp;gt;&lt;br /&gt;
* Press button &amp;quot;OK&amp;quot; and wait a while (you will see a new dialogue with showing the progress.&lt;br /&gt;
&lt;br /&gt;
Done. You have now a raster (e.g. corine_raster_scotland_clipped_5m.tif) which does not have land in areas, where OSM data has sea/ocean.&lt;br /&gt;
&lt;br /&gt;
=== Reclassifying a Raster Layer ===&lt;br /&gt;
WS3.0 uses CORINE landclass values.  If using data from other sources it needs to be reclassified to the correct values.  genVPB.py has an option to do this, but you may wish to do so manually.  &lt;br /&gt;
&lt;br /&gt;
To do this select &amp;lt;code&amp;gt;GRASS-&amp;gt;Raster-&amp;gt;r.reclass&amp;lt;/code&amp;gt; from the Processing Toolbox.&lt;br /&gt;
&lt;br /&gt;
Select the following options:&lt;br /&gt;
&lt;br /&gt;
* Input Raster Layer - correct layer, check CRS&lt;br /&gt;
* Reclass rules text - copy in the contents of https://sourceforge.net/p/flightgear/fgmeta/ci/next/tree/ws30/mappings/nlcd2019.txt.  Or an appropriate mapping from your landclass data to CORINE.  Note that you can also reference a file using the &amp;quot;File containing reclass rules&amp;quot; option. Note a mapping of 22 24 = 1 is the same as 22 and 24 = 1. For a range of 22 to 24 use 22 23 24 = 1.&lt;br /&gt;
* Reclassified - Select a new filename.&lt;br /&gt;
&lt;br /&gt;
(If this doesn't work a similar function is available in the Processing Toolbox under &amp;lt;code&amp;gt;Raster analysis-&amp;gt;Reclassify by table&amp;lt;/code&amp;gt;.  However this doesn't save your table once you close the dialog, and entries have to be manually entered individually which takes a lot of effort)&lt;br /&gt;
&lt;br /&gt;
=== Processing NLCD for USA using the Raster Calculator and tools in QGIS ===&lt;br /&gt;
&lt;br /&gt;
[[Processing_NLCD_for_USA_using_Raster_Calculator_and_tools_in_QGIS]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process NLCD for the USA ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_NLCD_for_the_USA]]&lt;br /&gt;
&lt;br /&gt;
=== Python script to process Sentinel-2 data ===&lt;br /&gt;
&lt;br /&gt;
[[Python_script_to_process_Sentinel-2_data]]&lt;br /&gt;
&lt;br /&gt;
===Generating the Terrain using osgdem===&lt;br /&gt;
Instead of using genVPB.py, you may wish to run osgdem directly.&lt;br /&gt;
&lt;br /&gt;
In the Windows/Docker platform you can send the generate tile command directly to osgdem.exe, one tile at a time.&lt;br /&gt;
&lt;br /&gt;
Using the NLCD raster processing convention from above, following is the the final step after creating the raster and entering bash shell with the windows version of &amp;quot;docker run...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
 osgdem --TERRAIN --image-ext png --RGBA --no-interpolate-imagery --disable-error-diffusion --geocentric --no-mip-mapping -t ./data/California-Southern_4326-84-hd-corrected.tiff -d ./SRTM-3/N32W115.hgt -b -115 32 -114 33 --PagedLOD -l 7 --radius-to-max-visible-distance-ratio 3 -o ./output/vpb/w120n30/w115n32/ws_w115n32.osgb&lt;br /&gt;
&lt;br /&gt;
Note: the --image-ext png --RGBA flags are critical to successfully building correctly placed landclasses in the final VPB generated scenery.&lt;br /&gt;
&lt;br /&gt;
If you prefer to run the scenery generation manually, running the VPB osgdem process is described in more detail here: [[Virtual Planet Builder#Running VPB]].&lt;br /&gt;
&lt;br /&gt;
After doing this you should have an output directory containing files of the form &amp;lt;code&amp;gt;output/vpb/w010n50/w004n50/ws_w004n50.osgb&amp;lt;/code&amp;gt;, plus a host of sub-directories. Each one of these is a 1x1 tile of terrain.  &lt;br /&gt;
&lt;br /&gt;
to leave the container simply type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
===Packaging the Scenery===&lt;br /&gt;
Once you have the terrain and line features they should be packaged in a scenery directory in vpb and Terrain sub-directories respectively.  E.g.&lt;br /&gt;
 MyCoolScenery/Terrain&lt;br /&gt;
 MyCoolScenery/vpb&lt;br /&gt;
It is good practise to document the data sources used in scenery generation.  Some source licenses require attribution of the original data source for anything derived, published or distributed.   &lt;br /&gt;
&lt;br /&gt;
To assist in fulfilling these license obligations, you can create a source.xml file in the scenery directory which includes attribution information.  This will then be available from within the simulator under Help-&amp;gt;Scenery Sources, and &amp;lt;u&amp;gt;may&amp;lt;/u&amp;gt; fulfil the attribution requirements of your license.  '''Note that you are responsible for fulfilling any license requirements from the data, not FlightGear'''.  &lt;br /&gt;
&lt;br /&gt;
The format of the file is straightforward:&lt;br /&gt;
 &amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;&lt;br /&gt;
 &amp;lt;PropertyList&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;Corine Land Cover (CLC) 2018, Version 2020_20u1&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;http://web.archive.org/web/20221112175615/https://land.copernicus.eu/pan-european/corine-land-cover/clc2018?tab=metadata%2A&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;GMES Open License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;NASADEM Merged DEM Global 1 arc second V001&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.earthdata.nasa.gov/&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Public Domain&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;name&amp;gt;OpenStreetMap&amp;lt;/name&amp;gt;&lt;br /&gt;
         &amp;lt;nowiki&amp;gt;&amp;lt;link&amp;gt;&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;https://www.openstreetmap.org/copyright&amp;lt;/nowiki&amp;gt;&amp;lt;nowiki&amp;gt;&amp;lt;/link&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
         &amp;lt;license&amp;gt;Open Data Commons Open Database License&amp;lt;/license&amp;gt;&lt;br /&gt;
     &amp;lt;nowiki&amp;gt;&amp;lt;/source&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
 &amp;lt;/PropertyList&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144285</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144285"/>
		<updated>2026-05-02T04:34:33Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 7)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144265</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144265"/>
		<updated>2026-04-30T21:06:36Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 7)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 3&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144264</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144264"/>
		<updated>2026-04-30T19:33:30Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 7)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 3&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144263</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144263"/>
		<updated>2026-04-30T19:33:13Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 1 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 7)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 9&lt;br /&gt;
size = 15&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144262</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144262"/>
		<updated>2026-04-30T18:18:00Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 2 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 2)*7'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 9&lt;br /&gt;
size = 15&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/Sentinel-2_AlabamaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144261</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144261"/>
		<updated>2026-04-30T18:17:18Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Part 1 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chunks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alabama/data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunk&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 2)*7'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 9&lt;br /&gt;
size = 15&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska//data/Sentinel-2/Sentinel-2_AlaskaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144260</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144260"/>
		<updated>2026-04-30T18:15:20Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Python script to process Sentinel-2 data */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
=== Part 1 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import (&lt;br /&gt;
    QgsRasterLayer,&lt;br /&gt;
    QgsProcessingFeedback,&lt;br /&gt;
    QgsRectangle&lt;br /&gt;
)&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from osgeo import gdal&lt;br /&gt;
import subprocess&lt;br /&gt;
import os&lt;br /&gt;
import math&lt;br /&gt;
&lt;br /&gt;
os.environ[&amp;quot;GDAL_PAM_ENABLED&amp;quot;] = &amp;quot;NO&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# -----------------------------&lt;br /&gt;
# USER SETTINGS&lt;br /&gt;
# -----------------------------&lt;br /&gt;
path = '/mnt/Windows/'&lt;br /&gt;
country = 'Alabama'&lt;br /&gt;
part = '16R_20250101-20251231'&lt;br /&gt;
&lt;br /&gt;
################################################# IMPORTANT #############################################&lt;br /&gt;
# Cuts Sentinel-2 chuncks into FlightGear tiles&lt;br /&gt;
# Eliminates water only tiles&lt;br /&gt;
# Reclassifies Sentenial-2 landcover types to match FlightGear&lt;br /&gt;
# Cleans up any urban area road artifacts&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/Sentinel-2/16R_20250101-20251231.tif&lt;br /&gt;
# Final output path to processed files is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska//data/Sentinel-2/tiles_16R_20250101-20251231/N30W086.tif * tile chunck&lt;br /&gt;
##########################################################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
input_raster = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_raster,&lt;br /&gt;
        'SOURCE_CRS': QgsCoordinateReferenceSystem('EPSG:5070'),&lt;br /&gt;
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 2)*7'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows requires&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 6: Cut into tiles #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_folder = path + country + '/data/Sentinel-2/tiles_' + part + '/'&lt;br /&gt;
tile_size_deg = 1.0&lt;br /&gt;
&lt;br /&gt;
os.makedirs(output_folder, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
rlayer = QgsRasterLayer(input_path, &amp;quot;input&amp;quot;)&lt;br /&gt;
if not rlayer.isValid():&lt;br /&gt;
    raise Exception(&amp;quot;Raster failed to load: &amp;quot; + input_path)&lt;br /&gt;
&lt;br /&gt;
ext = rlayer.extent()&lt;br /&gt;
xmin = ext.xMinimum()&lt;br /&gt;
xmax = ext.xMaximum()&lt;br /&gt;
ymin = ext.yMinimum()&lt;br /&gt;
ymax = ext.yMaximum()&lt;br /&gt;
&lt;br /&gt;
xmin_int = math.floor(xmin)&lt;br /&gt;
xmax_int = math.ceil(xmax)&lt;br /&gt;
ymin_int = math.floor(ymin)&lt;br /&gt;
ymax_int = math.ceil(ymax)&lt;br /&gt;
&lt;br /&gt;
feedback = QgsProcessingFeedback()&lt;br /&gt;
&lt;br /&gt;
for lat in range(ymin_int, ymax_int - 1):&lt;br /&gt;
    for lon in range(xmin_int, xmax_int - 1):&lt;br /&gt;
&lt;br /&gt;
        tile_xmin = lon&lt;br /&gt;
        tile_xmax = lon + tile_size_deg&lt;br /&gt;
        tile_ymin = lat&lt;br /&gt;
        tile_ymax = lat + tile_size_deg&lt;br /&gt;
&lt;br /&gt;
        if tile_xmax &amp;lt;= xmin or tile_xmin &amp;gt;= xmax:&lt;br /&gt;
            continue&lt;br /&gt;
        if tile_ymax &amp;lt;= ymin or tile_ymin &amp;gt;= ymax:&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        tile_extent = f&amp;quot;{tile_xmin},{tile_xmax},{tile_ymin},{tile_ymax}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        ns = &amp;quot;N&amp;quot; if (lat + 1) &amp;gt;= 0 else &amp;quot;S&amp;quot;&lt;br /&gt;
        ew = &amp;quot;E&amp;quot; if lon &amp;gt;= 0 else &amp;quot;W&amp;quot;&lt;br /&gt;
        out_name = f&amp;quot;{ns}{abs(lat+1):02d}{ew}{abs(lon):03d}.tif&amp;quot;&lt;br /&gt;
        out_path = os.path.join(output_folder, out_name)&lt;br /&gt;
&lt;br /&gt;
        tmp_tile = out_path.replace(&amp;quot;.tif&amp;quot;, &amp;quot;_tmp.tif&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        processing.run(&lt;br /&gt;
            &amp;quot;gdal:cliprasterbyextent&amp;quot;,&lt;br /&gt;
            {&lt;br /&gt;
                'INPUT': input_path,&lt;br /&gt;
                'PROJWIN': tile_extent,&lt;br /&gt;
                'NODATA': 0,&lt;br /&gt;
                'OPTIONS': '',&lt;br /&gt;
                'DATA_TYPE': 0,&lt;br /&gt;
                'OUTPUT': tmp_tile&lt;br /&gt;
            },&lt;br /&gt;
            feedback=feedback&lt;br /&gt;
        )&lt;br /&gt;
&lt;br /&gt;
        if not os.path.exists(tmp_tile) or os.path.getsize(tmp_tile) &amp;lt; 50:&lt;br /&gt;
            if os.path.exists(tmp_tile):&lt;br /&gt;
                os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (empty tile):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        ds = gdal.Open(tmp_tile)&lt;br /&gt;
        band = ds.GetRasterBand(1)&lt;br /&gt;
        arr = band.ReadAsArray()&lt;br /&gt;
&lt;br /&gt;
        # If every pixel is zero → water only&lt;br /&gt;
        if (arr == 0).all():&lt;br /&gt;
            os.remove(tmp_tile)&lt;br /&gt;
            print(&amp;quot;Skipped (all water):&amp;quot;, out_name)&lt;br /&gt;
            continue&lt;br /&gt;
&lt;br /&gt;
        os.rename(tmp_tile, out_path)&lt;br /&gt;
        print(&amp;quot;Created:&amp;quot;, out_path)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Tiling complete.&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Part 2 ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = 'N30W086';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 9&lt;br /&gt;
size = 15&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'nxxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/data/Sentinel-2/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state or country.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# N30W086.tif&lt;br /&gt;
# Full input path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/Sentinel-2/N30W086.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska//data/Sentinel-2/Sentinel-2_AlaskaN30W086_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 1: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 2: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144244</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144244"/>
		<updated>2026-04-30T02:28:06Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Optional Python script to process NLCD for the USA */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Optional Python script to process NLCD for the USA ===&lt;br /&gt;
You can optionally process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144243</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144243"/>
		<updated>2026-04-30T02:26:47Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Python script to process Sentinel-2 data */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = '/mnt/Windows/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = '16R_20250101-20251231';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 9&lt;br /&gt;
size = 15&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# 'xxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
# Also note that &amp;quot;/source/&amp;quot; and &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# Example beginning Sentinel-2 source:&lt;br /&gt;
# 16R_20250101-20251231.tif&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/source/Sentinel-2_Alaska141-140_60.tif&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/Sentinel-2_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
################################################################### Step 1: Warp land cover 4326 ############################################################&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
#import os&lt;br /&gt;
#print(&amp;quot;Exists:&amp;quot;, os.path.exists(input_path))&lt;br /&gt;
#print(&amp;quot;Dir exists:&amp;quot;, os.path.exists(os.path.dirname(input_path)))&lt;br /&gt;
#print(&amp;quot;Writable:&amp;quot;, os.access(os.path.dirname(input_path), os.W_OK))&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 2: Replace urban and clutter with grass ###################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 3: Reclass urban 2 ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 2)*7'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################ Step 4: Remove clutter and roads from urban ###########################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
############################################################## Step 5: Combine grass only and clean urban #########################################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command)&lt;br /&gt;
#Windows OS uses&lt;br /&gt;
#subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 6: Upsample to HD #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 7: Smooth all features in original dataset #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
################################################################## Step 8: Convert to 8Bit #################################################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144238</id>
		<title>Python script to process Sentinel-2 data</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_Sentinel-2_data&amp;diff=144238"/>
		<updated>2026-04-29T15:25:46Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Python script to process Sentinel-2 data */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Python script to process Sentinel-2 data ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess &lt;br /&gt;
&lt;br /&gt;
#Define input layer names, change according to your file names &lt;br /&gt;
&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
country = 'Alabama';&lt;br /&gt;
part = '16R_20250101-20251231';&lt;br /&gt;
&lt;br /&gt;
#ratio of upscale to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11 &lt;br /&gt;
&lt;br /&gt;
##################################IMPORTANT ###############################&lt;br /&gt;
#Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
#'xxxxxxxxxxxxxxxx.tif'&lt;br /&gt;
#Also note that &amp;quot;/source/&amp;quot; and &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
#I needed the existing path structure to process it by state.&lt;br /&gt;
#Example beginning Sentinel-2 source:&lt;br /&gt;
#16R_20250101-20251231.tif&lt;br /&gt;
#Full path and filename to source example is:&lt;br /&gt;
#G:/Scenery/ws3.0/Alaska/source/Sentinel-2_Alaska141-140_60.tif&lt;br /&gt;
#Final output path to last processed file is:&lt;br /&gt;
#G:/Scenery/ws3.0/Alaska/data/Sentinel-2_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
################################## Step 1: Warp land cover 4326 ##################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/source/' + part + '.tif'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',''&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',''&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Warp land_cover completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Land_Cover_4326)&amp;quot;) &lt;br /&gt;
&lt;br /&gt;
################################## Step 2: Replace urban and clutter with grass ##################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 1) * 41 + '&lt;br /&gt;
    '(A == 2) * 23 + '&lt;br /&gt;
    '(A == 3) * 44 + '&lt;br /&gt;
    '(A == 4) * 35 + '&lt;br /&gt;
    '(A == 5) * 21 + '&lt;br /&gt;
    '(A == 6) * 44 + '&lt;br /&gt;
    '(A == 7) * 26 + '&lt;br /&gt;
    '(A == 8) * 31 + '&lt;br /&gt;
    '(A == 9) * 34 + '&lt;br /&gt;
    '(A == 10) * 26 + '&lt;br /&gt;
    '(A == 11) * 26'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Replace urban and clutter with grass completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Grass-Only_4326)&amp;quot;) &lt;br /&gt;
&lt;br /&gt;
################################## Step 3: Reclass urban 21, 22, 23 or 24 ##################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2_' + country + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 2)*7'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3:  Reclass urban completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;) &lt;br /&gt;
&lt;br /&gt;
################################## Step 4: Remove clutter and roads from urban ##################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2_' + country + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',''&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',''&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Remove clutter and roads from urban. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Urban-Only_4326)&amp;quot;) &lt;br /&gt;
&lt;br /&gt;
################################## Step 5: Combine grass only and clean urban ##################################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + country + '/data/Sentinel-2_' + country + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + country + '/data/Sentinel-2_' + country + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Combined and clean completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean_4326)&amp;quot;) &lt;br /&gt;
&lt;br /&gt;
################################## Step 6: Upsample to HD ##################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2_' + country + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff' &lt;br /&gt;
&lt;br /&gt;
#Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform() &lt;br /&gt;
&lt;br /&gt;
#Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5]) &lt;br /&gt;
&lt;br /&gt;
#Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
defined at the begining of the script&lt;br /&gt;
#percentage = 8.0  # Adjust this as needed &lt;br /&gt;
&lt;br /&gt;
#Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage &lt;br /&gt;
&lt;br /&gt;
#Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6: Upsample to. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;) &lt;br /&gt;
&lt;br /&gt;
################################## Step 7: Smooth all features in original dataset ##################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2_' + country + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',''&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',''&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Smooth all features in original dataset completed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;) &lt;br /&gt;
&lt;br /&gt;
################################## Step 8: Convert to 8Bit ##################################&lt;br /&gt;
&lt;br /&gt;
input_path = path + country + '/data/Sentinel-2_' + country + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + country + '/data/Sentinel-2_' + country + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',''&lt;br /&gt;
		'EXTRA':'',''&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'Sentinel-2_&amp;quot; + country + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Convert to Compressed. (Sentinel-2_&amp;quot; + country + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
	<entry>
		<id>https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144237</id>
		<title>Python script to process NLCD for the USA</title>
		<link rel="alternate" type="text/html" href="https://wiki.flightgear.org/w/index.php?title=Python_script_to_process_NLCD_for_the_USA&amp;diff=144237"/>
		<updated>2026-04-29T15:24:46Z</updated>

		<summary type="html">&lt;p&gt;Wlbragg: /* Optional Python script to process NLCD for the USA */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Optional Python script to process NLCD for the USA ===&lt;br /&gt;
You can optionally process the NLCD by loading this script in the python console of the QGIS Desktop program.&lt;br /&gt;
This python script method produces more refined data than by using the python calculator method above and it is more automated.&lt;br /&gt;
If you run this script outside the python console you will need to modify it to locate the data you are processing.&lt;br /&gt;
If you save the NLCD tiff's using the same consistent naming convention used in this example it is fairly simple to generate final WS3.0 tiff's to use to generate the scenery.&lt;br /&gt;
&lt;br /&gt;
You will start with an original tree layer and a land cover layer from the MRLC.gov site. See the section &amp;quot;Obtaining NLCD&amp;quot; in the above &amp;quot;Step By Step Procedure for Processing NLCD for the USA using the Raster Calculator&amp;quot; topic.&lt;br /&gt;
&lt;br /&gt;
For example, to start with you will have a couple files named...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
&lt;br /&gt;
In the QGIS program the layer names you start with will be...&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Tree_Canopy_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
NLCD_2021_Land_Cover_Alaska141-140_60&lt;br /&gt;
&lt;br /&gt;
which will refer to the tiff files.&lt;br /&gt;
&lt;br /&gt;
Running this script will generate a bunch of intermediate files including the last file, that will be the final finished file for running in the VPB script or VPB build environment Docker image.&lt;br /&gt;
&lt;br /&gt;
The last file for this example for Alaska would be, NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326.tiff&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from qgis.core import QgsApplication, QgsCoordinateTransform, QgsProject, QgsRasterLayer, QgsCoordinateReferenceSystem, QgsProcessingException, QgsRasterBlock, QgsRectangle&lt;br /&gt;
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry&lt;br /&gt;
from qgis import processing&lt;br /&gt;
from processing.core.Processing import Processing&lt;br /&gt;
from osgeo import gdal, osr, ogr&lt;br /&gt;
import os&lt;br /&gt;
import numpy&lt;br /&gt;
import numpy as np&lt;br /&gt;
import subprocess&lt;br /&gt;
&lt;br /&gt;
# Define input layer names, change according to your file names&lt;br /&gt;
path = 'G:/Scenery/ws3.0/';&lt;br /&gt;
year = '2021';&lt;br /&gt;
state = 'Alaska';&lt;br /&gt;
part = '141-140_60';&lt;br /&gt;
&lt;br /&gt;
# Ratio of upsampling to smoothing&lt;br /&gt;
percentage = 8&lt;br /&gt;
size = 11&lt;br /&gt;
&lt;br /&gt;
################################## IMPORTANT ###############################&lt;br /&gt;
# Beginning filename of the NLCD from the source needs to be&lt;br /&gt;
# NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
# Also note that &amp;quot;/data/&amp;quot; is hardcoded into the path as well&lt;br /&gt;
# I needed the existing path structure to process it by state.&lt;br /&gt;
# For Alaska I used one lat and a few lon sections per chunck built.&lt;br /&gt;
# I will likely do the entire NLCD coverage area the same in future runs.&lt;br /&gt;
# Example beginning NLCD source:&lt;br /&gt;
# NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Full path and filename to source example is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Tree_Canopy_Alaska141-140_60.tiff&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/source/NLCD_2021_Land_Cover_Alaska141-140_60.tiff&lt;br /&gt;
# Final output path to last processed file is:&lt;br /&gt;
# G:/Scenery/ws3.0/Alaska/data/NLCD_2021_Alaska141-140_60_Smoothed-HD-Compressed_4326&lt;br /&gt;
############################################################################&lt;br /&gt;
&lt;br /&gt;
##################### Step 1: Reclass tree canopy to one landcover type 41, 42, or 43 #####################&lt;br /&gt;
#FG                     NLCD&lt;br /&gt;
#23 DeciduousBroadCover 41&lt;br /&gt;
#24 EvergreenForest     42&lt;br /&gt;
#25 MixedForest         43&lt;br /&gt;
&lt;br /&gt;
# Define input and output paths&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Tree_Canopy_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '((A &amp;gt; 0) &amp;amp; (A &amp;lt; 255)) * 43 + '&lt;br /&gt;
    '(A &amp;lt;= 0) * A'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 1: Calculate result_tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Trees-Combined)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 2: Warp tree canopy to 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Trees-Combined.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 2: Warp tree_canopy completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Canopy_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 3: Warp land cover 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/source/NLCD_' + year +  '_Land_Cover_' + state + part + '.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;gdal:warpreproject&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'INPUT':input_path,&lt;br /&gt;
        'SOURCE_CRS':None,&lt;br /&gt;
        'TARGET_CRS':QgsCoordinateReferenceSystem('EPSG:4326'),&lt;br /&gt;
        'RESAMPLING':0,&lt;br /&gt;
        'NODATA':None,&lt;br /&gt;
        'TARGET_RESOLUTION':None,&lt;br /&gt;
        'OPTIONS':'',&lt;br /&gt;
        'DATA_TYPE':1,  # GDAL GDT_Byte&lt;br /&gt;
        'TARGET_EXTENT':None,&lt;br /&gt;
        'TARGET_EXTENT_CRS':None,&lt;br /&gt;
        'MULTITHREADING':False,&lt;br /&gt;
        'EXTRA':'',&lt;br /&gt;
        'OUTPUT':output_path&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 3: Warp land_cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Land_Cover_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 4: Combine land cover 4326 and tree canopy 4326 #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Canopy_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Land_Cover_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(((B &amp;gt; 0) &amp;amp; (B &amp;lt; 255)) &amp;amp; (B != 41) &amp;amp; (B != 42) &amp;amp; (B != 43) &amp;amp; (A &amp;gt; 0)) * A + '&lt;br /&gt;
    '((B == 41) | (B == 42) | (B == 43) | (A &amp;lt;= 0)) * B'&lt;br /&gt;
   )&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 4: Combine tree canopy and land cover completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Tree_Land_Combined_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 5: Replace urban and clutter with grass #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 0) * 44 + '&lt;br /&gt;
    '(A == 11) * 41 + '&lt;br /&gt;
    '(A == 12) * 34 + '&lt;br /&gt;
    '(A == 21) * 26 + '&lt;br /&gt;
    '(A == 22) * 26 + '&lt;br /&gt;
    '(A == 23) * 26 + '&lt;br /&gt;
    '(A == 24) * 26 + '&lt;br /&gt;
    '(A == 31) * 27 + '&lt;br /&gt;
    '(A == 41) * 23 + '&lt;br /&gt;
    '(A == 42) * 24 + '&lt;br /&gt;
    '(A == 43) * 25 + '&lt;br /&gt;
    '(A == 51) * 30 + '&lt;br /&gt;
    '(A == 52) * 29 + '&lt;br /&gt;
    '(A == 71) * 26 + '&lt;br /&gt;
    '(A == 72) * 32 + '&lt;br /&gt;
    '(A == 73) * 31 + '&lt;br /&gt;
    '(A == 74) * 31 + '&lt;br /&gt;
    '(A == 75) * 32 + '&lt;br /&gt;
    '(A == 81) * 18 + '&lt;br /&gt;
    '(A == 82) * 19 + '&lt;br /&gt;
    '(A == 90) * 25 + '&lt;br /&gt;
    '(A == 95) * 35'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 5: Replace urban and clutter with grass completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Grass-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 6: Reclass urban 21, 22, 23 or 24 #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Tree_Land_Combined_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
    '(A == 21)*10 + '&lt;br /&gt;
    '(A == 22)*1 + '&lt;br /&gt;
    '(A == 23)*1 + '&lt;br /&gt;
    '(A == 24)*2'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 6:  Reclass urban completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Reclassed-Urban_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 7: Remove clutter and roads from urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Reclassed-Urban_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':1, #1=median, 2=mode&lt;br /&gt;
        'size':7,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
        'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_PARAMETER':None,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 7: Remove clutter and roads from urban. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Urban-Only_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 8: Combine grass only and clean urban #####################&lt;br /&gt;
&lt;br /&gt;
input_path_a = path + state + '/data/NLCD_' + year + '_' + state + part + '_Urban-Only_4326.tiff'&lt;br /&gt;
input_path_b = path + state + '/data/NLCD_' + year + '_' + state + part + '_Grass-Only_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year + '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
expression = (&lt;br /&gt;
   '((A &amp;gt; 0) &amp;amp; (B &amp;gt; 0)) * A + '&lt;br /&gt;
   '((A &amp;lt;= 0) | (B &amp;lt;= 0)) * B'&lt;br /&gt;
)&lt;br /&gt;
command = [&lt;br /&gt;
    'gdal_calc.py',&lt;br /&gt;
    '-A', input_path_a,&lt;br /&gt;
    '-B', input_path_b,&lt;br /&gt;
    '--outfile', output_path,&lt;br /&gt;
    '--calc', expression,&lt;br /&gt;
    '--NoDataValue', '0',&lt;br /&gt;
    '--type', 'Byte'&lt;br /&gt;
]&lt;br /&gt;
subprocess.run(command, shell=True)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 8: Combined and clean completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 9: Upsample to HD #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
# Open the original raster&lt;br /&gt;
ds = gdal.Open(input_path)&lt;br /&gt;
gt = ds.GetGeoTransform()&lt;br /&gt;
&lt;br /&gt;
# Extract the original resolution&lt;br /&gt;
original_xRes = gt[1]&lt;br /&gt;
original_yRes = abs(gt[5])&lt;br /&gt;
&lt;br /&gt;
# Define the percentage to resize by (e.g., 0.50 for 50%, 2.0 for 200%)&lt;br /&gt;
#defined at the beginning of the script&lt;br /&gt;
#percentage = 8.0&lt;br /&gt;
&lt;br /&gt;
# Calculate the new resolution&lt;br /&gt;
new_xRes = original_xRes / percentage&lt;br /&gt;
new_yRes = original_yRes / percentage&lt;br /&gt;
&lt;br /&gt;
# Perform the warp (resampling)&lt;br /&gt;
gdal.Warp(&lt;br /&gt;
    output_path,&lt;br /&gt;
    input_path,&lt;br /&gt;
    xRes=new_xRes,&lt;br /&gt;
    yRes=new_yRes,&lt;br /&gt;
    outputType=gdal.GDT_Byte  # Set the output type to uint8&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 9: Upsample to. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Combined-Clean-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 10: Smooth all features in original dataset #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Combined-Clean-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
    &amp;quot;grass7:r.neighbors&amp;quot;,&lt;br /&gt;
    {&lt;br /&gt;
        'input':input_path,&lt;br /&gt;
        'selection':None,&lt;br /&gt;
        'method':3, #1=median, 3=mode&lt;br /&gt;
        'size':size,&lt;br /&gt;
        'gauss':None,&lt;br /&gt;
        'quantile':'',&lt;br /&gt;
        '-c':False,&lt;br /&gt;
        '-a':False,&lt;br /&gt;
		'weight':'',&lt;br /&gt;
        'output':output_path,&lt;br /&gt;
        'GRASS_REGION_CELLSIZE_PARAMETER':0,&lt;br /&gt;
        'GRASS_RASTER_FORMAT_OPT': 'TYPE=BYTE',&lt;br /&gt;
        'GRASS_RASTER_FORMAT_META':''&lt;br /&gt;
    }&lt;br /&gt;
) &lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 10: Smooth all features in original dataset completed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
##################### Step 11: Compress and insure 8Bit #####################&lt;br /&gt;
&lt;br /&gt;
input_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD_4326.tiff'&lt;br /&gt;
output_path = path + state + '/data/NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326.tiff'&lt;br /&gt;
&lt;br /&gt;
processing.run(&lt;br /&gt;
	&amp;quot;gdal:translate&amp;quot;,&lt;br /&gt;
	{&lt;br /&gt;
		'INPUT':input_path,&lt;br /&gt;
		'TARGET_CRS':None,&lt;br /&gt;
		'NODATA':0,&lt;br /&gt;
		'COPY_SUBDATASETS':False,&lt;br /&gt;
		'OPTIONS':'',&lt;br /&gt;
		'EXTRA':'',&lt;br /&gt;
		'DATA_TYPE':1,&lt;br /&gt;
		'OUTPUT':output_path,&lt;br /&gt;
		'OPTIONS': 'COMPRESS=LZW'&lt;br /&gt;
	}&lt;br /&gt;
)&lt;br /&gt;
&lt;br /&gt;
result_bit_conversion_layer = QgsRasterLayer(output_path, 'NLCD_' + year +  '_' + state + part + '_Smoothed-HD-Compressed_4326')&lt;br /&gt;
QgsProject.instance().addMapLayer(result_bit_conversion_layer)&lt;br /&gt;
&lt;br /&gt;
print(&amp;quot;Step 11: Convert to Compressed. (NLCD_&amp;quot; + year +  &amp;quot;_&amp;quot; + state + part + &amp;quot;_Smoothed-HD-Compressed_4326)&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>Wlbragg</name></author>
	</entry>
</feed>