[{"data":1,"prerenderedAt":1620},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Frun-viewshed-analysis-pyqgis":3},{"id":4,"title":5,"body":6,"description":1609,"extension":1610,"meta":1611,"navigation":186,"path":1616,"seo":1617,"stem":1618,"__hash__":1619},"docs\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Frun-viewshed-analysis-pyqgis\u002Findex.md","Run a Viewshed Analysis in PyQGIS",{"type":7,"value":8,"toc":1595},"minimark",[9,13,17,26,112,117,139,143,146,420,443,447,450,538,545,549,555,811,836,840,843,1190,1203,1207,1210,1259,1267,1271,1274,1442,1452,1456,1464,1468,1496,1500,1509,1513,1524,1530,1541,1547,1553,1559,1563,1591],[10,11,5],"h1",{"id":12},"run-a-viewshed-analysis-in-pyqgis",[14,15,16],"p",{},"A viewshed is the area visible from a point: what a lookout tower sees, which homes will see a proposed wind turbine, where a mast's antenna has line of sight, how much of a valley a camera covers. The calculation is conceptually simple — trace a line from the observer to every cell of an elevation model and test whether terrain gets in the way — and the result depends heavily on choices that are easy to overlook: observer and target heights, the maximum distance, earth curvature, and above all whether the elevation model includes buildings and trees.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002F","Terrain & Interpolation Analysis",". It runs a viewshed with GDAL, sets heights and distance deliberately, accounts for curvature with GRASS, combines several observers into a cumulative viewshed, and explains the limits of the input surface.",[14,27,28],{},[29,30,35,39,43,50,59,66,74,81,85,91,97,103,107],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","A terrain profile showing an observer's lines of sight reaching some slopes, blocked behind a crest, and reaching more when target height is raised","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Line of sight over terrain",[40,41,42],"desc",{},"A terrain profile with an observer on a hill at 1.6 metres above ground and targets beyond. Lines of sight from the observer reach the near slope and the far ridge but are blocked from the valley behind a crest, which lies in a shadow. Raising the target height, for example to the top of a 100 metre turbine, makes cells visible that were hidden at ground level.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","280","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","What the terrain hides",[60,61],"path",{"d":62,"fill":63,"stroke":64,"style":65},"M 20 230 L 120 140 L 200 150 L 300 110 L 380 200 L 470 220 L 560 150 L 660 120 L 740 170","none","#59645f","stroke-width:3",[67,68],"circle",{"cx":69,"cy":70,"r":71,"fill":72,"stroke":56,"style":73},"120","128","6","#2563eb","stroke-width:1",[75,76],"line",{"x1":69,"y1":70,"x2":77,"y2":78,"stroke":79,"style":80},"300","108","#15803d","stroke-width:1.5",[75,82],{"x1":69,"y1":70,"x2":83,"y2":84,"stroke":79,"style":80},"660","116",[75,86],{"x1":69,"y1":70,"x2":87,"y2":88,"stroke":89,"style":90},"470","216","#b91c1c","stroke-width:1.5;stroke-dasharray:5 4",[44,92],{"x":53,"y":93,"width":69,"height":94,"rx":95,"fill":96,"stroke":89,"style":73},"196","30","2","#fdf2e2",[51,98,102],{"x":99,"y":100,"style":101,"fill":89,"textAnchor":57},"440","252","text-anchor:middle;font-size:10.5px;font-family:sans-serif","hidden behind the crest",[51,104,106],{"x":69,"y":105,"style":101,"fill":72,"textAnchor":57},"112","observer + 1.6 m",[51,108,111],{"x":83,"y":109,"style":101,"fill":110,"textAnchor":57},"104","#166534","visible ridge",[113,114,116],"h2",{"id":115},"prerequisites","Prerequisites",[118,119,120,133,136],"ul",{},[121,122,123,124,128,129,132],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series. ",[125,126,127],"code",{},"gdal:viewshed"," is available from QGIS 3.28 with GDAL 3.1 or newer; GRASS's ",[125,130,131],{},"r.viewshed"," is available when the GRASS provider is installed.",[121,134,135],{},"A DEM in a projected CRS with metre units — or better, a digital surface model (DSM) that includes buildings and vegetation, as explained below.",[121,137,138],{},"Observer locations as coordinates or a point layer.",[113,140,142],{"id":141},"run-a-single-viewshed-with-gdal","Run a single viewshed with GDAL",[14,144,145],{},"The GDAL algorithm takes the elevation raster, the observer position and heights, and writes a raster where visible cells have one value and invisible cells another.",[147,148,153],"pre",{"className":149,"code":150,"language":151,"meta":152,"style":152},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsRasterLayer, QgsPointXY, QgsProject\n\ndem = QgsRasterLayer(\"\u002Fdata\u002Fterrain\u002Fdsm_2m.tif\", \"DSM\")\nobserver = QgsPointXY(571244.0, 5934019.0)\n\nvs = processing.run(\"gdal:viewshed\", {\n    \"INPUT\": dem,\n    \"BAND\": 1,\n    \"OBSERVER\": f\"{observer.x()},{observer.y()} [{dem.crs().authid()}]\",\n    \"OBSERVER_HEIGHT\": 1.6,\n    \"TARGET_HEIGHT\": 0.0,\n    \"MAX_DISTANCE\": 5000,\n    \"OUTPUT\": \"\u002Fdata\u002Fresults\u002Fviewshed_tower.tif\",\n})[\"OUTPUT\"]\n\nresult = QgsRasterLayer(vs, \"viewshed (tower)\")\nQgsProject.instance().addMapLayer(result)\n","python","",[125,154,155,167,181,188,213,235,240,257,266,281,329,342,355,368,381,393,398,414],{"__ignoreMap":152},[156,157,159,163],"span",{"class":75,"line":158},1,[156,160,162],{"class":161},"snl16","import",[156,164,166],{"class":165},"s95oV"," processing\n",[156,168,170,173,176,178],{"class":75,"line":169},2,[156,171,172],{"class":161},"from",[156,174,175],{"class":165}," qgis.core ",[156,177,162],{"class":161},[156,179,180],{"class":165}," QgsRasterLayer, QgsPointXY, QgsProject\n",[156,182,184],{"class":75,"line":183},3,[156,185,187],{"emptyLinePlaceholder":186},true,"\n",[156,189,191,194,197,200,204,207,210],{"class":75,"line":190},4,[156,192,193],{"class":165},"dem ",[156,195,196],{"class":161},"=",[156,198,199],{"class":165}," QgsRasterLayer(",[156,201,203],{"class":202},"sU2Wk","\"\u002Fdata\u002Fterrain\u002Fdsm_2m.tif\"",[156,205,206],{"class":165},", ",[156,208,209],{"class":202},"\"DSM\"",[156,211,212],{"class":165},")\n",[156,214,216,219,221,224,228,230,233],{"class":75,"line":215},5,[156,217,218],{"class":165},"observer ",[156,220,196],{"class":161},[156,222,223],{"class":165}," QgsPointXY(",[156,225,227],{"class":226},"sDLfK","571244.0",[156,229,206],{"class":165},[156,231,232],{"class":226},"5934019.0",[156,234,212],{"class":165},[156,236,238],{"class":75,"line":237},6,[156,239,187],{"emptyLinePlaceholder":186},[156,241,243,246,248,251,254],{"class":75,"line":242},7,[156,244,245],{"class":165},"vs ",[156,247,196],{"class":161},[156,249,250],{"class":165}," processing.run(",[156,252,253],{"class":202},"\"gdal:viewshed\"",[156,255,256],{"class":165},", {\n",[156,258,260,263],{"class":75,"line":259},8,[156,261,262],{"class":202},"    \"INPUT\"",[156,264,265],{"class":165},": dem,\n",[156,267,269,272,275,278],{"class":75,"line":268},9,[156,270,271],{"class":202},"    \"BAND\"",[156,273,274],{"class":165},": ",[156,276,277],{"class":226},"1",[156,279,280],{"class":165},",\n",[156,282,284,287,289,292,295,298,301,304,307,309,312,314,317,319,322,324,327],{"class":75,"line":283},10,[156,285,286],{"class":202},"    \"OBSERVER\"",[156,288,274],{"class":165},[156,290,291],{"class":161},"f",[156,293,294],{"class":202},"\"",[156,296,297],{"class":226},"{",[156,299,300],{"class":165},"observer.x()",[156,302,303],{"class":226},"}",[156,305,306],{"class":202},",",[156,308,297],{"class":226},[156,310,311],{"class":165},"observer.y()",[156,313,303],{"class":226},[156,315,316],{"class":202}," [",[156,318,297],{"class":226},[156,320,321],{"class":165},"dem.crs().authid()",[156,323,303],{"class":226},[156,325,326],{"class":202},"]\"",[156,328,280],{"class":165},[156,330,332,335,337,340],{"class":75,"line":331},11,[156,333,334],{"class":202},"    \"OBSERVER_HEIGHT\"",[156,336,274],{"class":165},[156,338,339],{"class":226},"1.6",[156,341,280],{"class":165},[156,343,345,348,350,353],{"class":75,"line":344},12,[156,346,347],{"class":202},"    \"TARGET_HEIGHT\"",[156,349,274],{"class":165},[156,351,352],{"class":226},"0.0",[156,354,280],{"class":165},[156,356,358,361,363,366],{"class":75,"line":357},13,[156,359,360],{"class":202},"    \"MAX_DISTANCE\"",[156,362,274],{"class":165},[156,364,365],{"class":226},"5000",[156,367,280],{"class":165},[156,369,371,374,376,379],{"class":75,"line":370},14,[156,372,373],{"class":202},"    \"OUTPUT\"",[156,375,274],{"class":165},[156,377,378],{"class":202},"\"\u002Fdata\u002Fresults\u002Fviewshed_tower.tif\"",[156,380,280],{"class":165},[156,382,384,387,390],{"class":75,"line":383},15,[156,385,386],{"class":165},"})[",[156,388,389],{"class":202},"\"OUTPUT\"",[156,391,392],{"class":165},"]\n",[156,394,396],{"class":75,"line":395},16,[156,397,187],{"emptyLinePlaceholder":186},[156,399,401,404,406,409,412],{"class":75,"line":400},17,[156,402,403],{"class":165},"result ",[156,405,196],{"class":161},[156,407,408],{"class":165}," QgsRasterLayer(vs, ",[156,410,411],{"class":202},"\"viewshed (tower)\"",[156,413,212],{"class":165},[156,415,417],{"class":75,"line":416},18,[156,418,419],{"class":165},"QgsProject.instance().addMapLayer(result)\n",[14,421,422,426,427,430,431,434,435,438,439,442],{},[423,424,425],"strong",{},"Breakdown:"," ",[125,428,429],{},"OBSERVER_HEIGHT"," is added to the ground elevation at the observer — 1.6 m for eye level, the mast height for an antenna. ",[125,432,433],{},"TARGET_HEIGHT"," is added at every target cell: 0 asks \"can I see the ground there?\", while 100 asks \"can I see a 100 m turbine there?\" — a very different question with a much larger answer. ",[125,436,437],{},"MAX_DISTANCE"," bounds the computation; beyond 10–20 km, atmospheric visibility and curvature dominate. The output is 255 for visible and 0 for invisible by default; check ",[125,440,441],{},"processing.algorithmHelp(\"gdal:viewshed\")"," for the exact parameter names and value options on your version.",[113,444,446],{"id":445},"choose-heights-and-distance-deliberately","Choose heights and distance deliberately",[14,448,449],{},"Observer and target heights encode the question being asked, and they change results dramatically.",[14,451,452],{},[29,453,456,459,462,465,468,478,484,489,493,497,501,505,509,513,517,521,525,529,532,535],{"viewBox":454,"role":32,"ariaLabel":455,"xmlns":34},"0 0 760 250","Common viewshed questions and their observer and target heights, including reciprocal visibility for turbines",[36,457,458],{},"Heights encode the question",[40,460,461],{},"Three common questions. Can a walker see the ground: observer 1.6 metres, target 0. Can a resident see the turbine's hub: observer 1.6 metres at each home, target 120 metres. Can a mast's antenna reach a receiver: observer the antenna height, target the receiver height. Swapping observer and target in the turbine case gives reciprocal visibility for many homes in one run.",[44,463],{"x":46,"y":46,"width":47,"height":464,"fill":49},"250",[51,466,467],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"What are you asking?",[44,469],{"x":470,"y":471,"width":472,"height":473,"rx":474,"fill":475,"stroke":476,"style":477},"24","56","222","170","8","#eef7f4","#0f766e","stroke-width:2",[51,479,483],{"x":480,"y":481,"style":482,"fill":476,"textAnchor":57},"135","102.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","walker",[51,485,488],{"x":480,"y":486,"style":101,"fill":487,"textAnchor":57},"130.78","#2f3b35","observer 1.6 m",[51,490,492],{"x":480,"y":491,"style":101,"fill":487,"textAnchor":57},"158.78","target 0 m",[51,494,496],{"x":480,"y":495,"style":101,"fill":64,"textAnchor":57},"186.78","sees the ground",[44,498],{"x":499,"y":471,"width":472,"height":473,"rx":474,"fill":96,"stroke":500,"style":477},"269","#b45309",[51,502,504],{"x":53,"y":503,"style":482,"fill":500,"textAnchor":57},"88.78","turbine",[51,506,508],{"x":53,"y":507,"style":101,"fill":487,"textAnchor":57},"116.78","observer: turbine",[51,510,512],{"x":53,"y":511,"style":101,"fill":487,"textAnchor":57},"144.78","120 m hub",[51,514,516],{"x":53,"y":515,"style":101,"fill":487,"textAnchor":57},"172.78","target 1.6 m",[51,518,520],{"x":53,"y":519,"style":101,"fill":64,"textAnchor":57},"200.78","homes that see it",[44,522],{"x":523,"y":471,"width":472,"height":473,"rx":474,"fill":524,"stroke":72,"style":477},"514","#eff3ff",[51,526,528],{"x":527,"y":481,"style":482,"fill":72,"textAnchor":57},"625","radio link",[51,530,531],{"x":527,"y":486,"style":101,"fill":487,"textAnchor":57},"observer: antenna",[51,533,534],{"x":527,"y":491,"style":101,"fill":487,"textAnchor":57},"target: receiver",[51,536,537],{"x":527,"y":495,"style":101,"fill":64,"textAnchor":57},"line of sight",[14,539,540,541,544],{},"For turbines and masts, run the viewshed ",[542,543,172],"em",{}," the structure with the structure's height as observer height and eye level as target height: the result shows every place from which the structure is visible, in one run, instead of one run per home. Visibility is reciprocal in this geometric sense, provided heights are swapped consistently. For maximum distance, use the distance at which the object is still perceptible — a turbine is visible tens of kilometres away, a person a few hundred metres.",[113,546,548],{"id":547},"account-for-earth-curvature","Account for earth curvature",[14,550,551,552,554],{},"Over long distances the earth curves away below the line of sight — about 8 m at 10 km, 31 m at 20 km — and atmospheric refraction bends light back slightly. GRASS's ",[125,553,131],{}," includes both.",[147,556,558],{"className":149,"code":557,"language":151,"meta":152,"style":152},"from qgis.core import QgsApplication\n\nids = [a.id() for a in QgsApplication.processingRegistry().algorithms() if a.id().endswith(\"r.viewshed\")]\nalg = ids[0] if ids else None\nprint(\"using\", alg)\n\ngrass_vs = processing.run(alg, {\n    \"input\": dem,\n    \"coordinates\": f\"{observer.x()},{observer.y()}\",\n    \"observer_elevation\": 120,\n    \"target_elevation\": 1.6,\n    \"max_distance\": 20000,\n    \"refraction_coeff\": 0.14286,\n    \"-c\": True,                      # consider earth curvature\n    \"-r\": True,                      # consider refraction\n    \"-b\": True,                      # binary output: 1 visible, 0 not\n    \"output\": \"\u002Fdata\u002Fresults\u002Fviewshed_turbine_curved.tif\",\n})[\"output\"]\n",[125,559,560,571,575,609,635,649,653,663,670,699,710,721,733,745,762,776,790,802],{"__ignoreMap":152},[156,561,562,564,566,568],{"class":75,"line":158},[156,563,172],{"class":161},[156,565,175],{"class":165},[156,567,162],{"class":161},[156,569,570],{"class":165}," QgsApplication\n",[156,572,573],{"class":75,"line":169},[156,574,187],{"emptyLinePlaceholder":186},[156,576,577,580,582,585,588,591,594,597,600,603,606],{"class":75,"line":183},[156,578,579],{"class":165},"ids ",[156,581,196],{"class":161},[156,583,584],{"class":165}," [a.id() ",[156,586,587],{"class":161},"for",[156,589,590],{"class":165}," a ",[156,592,593],{"class":161},"in",[156,595,596],{"class":165}," QgsApplication.processingRegistry().algorithms() ",[156,598,599],{"class":161},"if",[156,601,602],{"class":165}," a.id().endswith(",[156,604,605],{"class":202},"\"r.viewshed\"",[156,607,608],{"class":165},")]\n",[156,610,611,614,616,619,621,624,626,629,632],{"class":75,"line":190},[156,612,613],{"class":165},"alg ",[156,615,196],{"class":161},[156,617,618],{"class":165}," ids[",[156,620,46],{"class":226},[156,622,623],{"class":165},"] ",[156,625,599],{"class":161},[156,627,628],{"class":165}," ids ",[156,630,631],{"class":161},"else",[156,633,634],{"class":226}," None\n",[156,636,637,640,643,646],{"class":75,"line":215},[156,638,639],{"class":226},"print",[156,641,642],{"class":165},"(",[156,644,645],{"class":202},"\"using\"",[156,647,648],{"class":165},", alg)\n",[156,650,651],{"class":75,"line":237},[156,652,187],{"emptyLinePlaceholder":186},[156,654,655,658,660],{"class":75,"line":242},[156,656,657],{"class":165},"grass_vs ",[156,659,196],{"class":161},[156,661,662],{"class":165}," processing.run(alg, {\n",[156,664,665,668],{"class":75,"line":259},[156,666,667],{"class":202},"    \"input\"",[156,669,265],{"class":165},[156,671,672,675,677,679,681,683,685,687,689,691,693,695,697],{"class":75,"line":268},[156,673,674],{"class":202},"    \"coordinates\"",[156,676,274],{"class":165},[156,678,291],{"class":161},[156,680,294],{"class":202},[156,682,297],{"class":226},[156,684,300],{"class":165},[156,686,303],{"class":226},[156,688,306],{"class":202},[156,690,297],{"class":226},[156,692,311],{"class":165},[156,694,303],{"class":226},[156,696,294],{"class":202},[156,698,280],{"class":165},[156,700,701,704,706,708],{"class":75,"line":283},[156,702,703],{"class":202},"    \"observer_elevation\"",[156,705,274],{"class":165},[156,707,69],{"class":226},[156,709,280],{"class":165},[156,711,712,715,717,719],{"class":75,"line":331},[156,713,714],{"class":202},"    \"target_elevation\"",[156,716,274],{"class":165},[156,718,339],{"class":226},[156,720,280],{"class":165},[156,722,723,726,728,731],{"class":75,"line":344},[156,724,725],{"class":202},"    \"max_distance\"",[156,727,274],{"class":165},[156,729,730],{"class":226},"20000",[156,732,280],{"class":165},[156,734,735,738,740,743],{"class":75,"line":357},[156,736,737],{"class":202},"    \"refraction_coeff\"",[156,739,274],{"class":165},[156,741,742],{"class":226},"0.14286",[156,744,280],{"class":165},[156,746,747,750,752,755,758],{"class":75,"line":370},[156,748,749],{"class":202},"    \"-c\"",[156,751,274],{"class":165},[156,753,754],{"class":226},"True",[156,756,757],{"class":165},",                      ",[156,759,761],{"class":760},"sjoCn","# consider earth curvature\n",[156,763,764,767,769,771,773],{"class":75,"line":383},[156,765,766],{"class":202},"    \"-r\"",[156,768,274],{"class":165},[156,770,754],{"class":226},[156,772,757],{"class":165},[156,774,775],{"class":760},"# consider refraction\n",[156,777,778,781,783,785,787],{"class":75,"line":395},[156,779,780],{"class":202},"    \"-b\"",[156,782,274],{"class":165},[156,784,754],{"class":226},[156,786,757],{"class":165},[156,788,789],{"class":760},"# binary output: 1 visible, 0 not\n",[156,791,792,795,797,800],{"class":75,"line":400},[156,793,794],{"class":202},"    \"output\"",[156,796,274],{"class":165},[156,798,799],{"class":202},"\"\u002Fdata\u002Fresults\u002Fviewshed_turbine_curved.tif\"",[156,801,280],{"class":165},[156,803,804,806,809],{"class":75,"line":416},[156,805,386],{"class":165},[156,807,808],{"class":202},"\"output\"",[156,810,392],{"class":165},[14,812,813,815,816,819,820,823,824,827,828,830,831,835],{},[423,814,425],{}," The GRASS provider's id prefix has changed between releases (",[125,817,818],{},"grass7:"," on older ones), so finding the algorithm by its suffix keeps the script portable. The ",[125,821,822],{},"-c"," and ",[125,825,826],{},"-r"," flags switch on curvature and refraction, with the standard coefficient of 1\u002F7. The binary flag gives a simple 1\u002F0 output that is easy to combine; without it, ",[125,829,131],{}," writes the vertical angle to each visible cell, which is useful for prominence studies. Running GRASS algorithms from Python is covered generally in ",[21,832,834],{"href":833},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-grass-and-saga-algorithms-pyqgis\u002F","running GRASS and SAGA algorithms",".",[113,837,839],{"id":838},"combine-several-observers","Combine several observers",[14,841,842],{},"Coverage questions involve many observers — all cameras on a site, every tower in a fire-watch network, every turbine in a wind farm. A cumulative viewshed counts, for each cell, how many observers see it.",[147,844,846],{"className":149,"code":845,"language":151,"meta":152,"style":152},"from qgis.core import QgsVectorLayer\nimport numpy as np\nfrom osgeo import gdal\n\ntowers = QgsVectorLayer(\"\u002Fdata\u002Fsites\u002Flookout_towers.gpkg\", \"towers\", \"ogr\")\npaths = []\nfor f in towers.getFeatures():\n    p = f.geometry().asPoint()\n    out = f\"\u002Fdata\u002Fresults\u002Fvs_{f['tower_id']}.tif\"\n    processing.run(\"gdal:viewshed\", {\n        \"INPUT\": dem, \"BAND\": 1, \"OBSERVER\": f\"{p.x()},{p.y()} [{dem.crs().authid()}]\",\n        \"OBSERVER_HEIGHT\": f[\"height_m\"], \"TARGET_HEIGHT\": 0, \"MAX_DISTANCE\": 15000,\n        \"OUTPUT\": out})\n    paths.append(out)\n\nstack = None\nfor path in paths:\n    arr = (gdal.Open(path).ReadAsArray() > 0).astype(\"uint8\")\n    stack = arr if stack is None else stack + arr\nprint(\"cells seen by at least one tower:\", int((stack > 0).sum()))\n",[125,847,848,859,872,884,888,913,923,935,945,974,983,1037,1070,1078,1083,1087,1096,1108,1132,1165],{"__ignoreMap":152},[156,849,850,852,854,856],{"class":75,"line":158},[156,851,172],{"class":161},[156,853,175],{"class":165},[156,855,162],{"class":161},[156,857,858],{"class":165}," QgsVectorLayer\n",[156,860,861,863,866,869],{"class":75,"line":169},[156,862,162],{"class":161},[156,864,865],{"class":165}," numpy ",[156,867,868],{"class":161},"as",[156,870,871],{"class":165}," np\n",[156,873,874,876,879,881],{"class":75,"line":183},[156,875,172],{"class":161},[156,877,878],{"class":165}," osgeo ",[156,880,162],{"class":161},[156,882,883],{"class":165}," gdal\n",[156,885,886],{"class":75,"line":190},[156,887,187],{"emptyLinePlaceholder":186},[156,889,890,893,895,898,901,903,906,908,911],{"class":75,"line":215},[156,891,892],{"class":165},"towers ",[156,894,196],{"class":161},[156,896,897],{"class":165}," QgsVectorLayer(",[156,899,900],{"class":202},"\"\u002Fdata\u002Fsites\u002Flookout_towers.gpkg\"",[156,902,206],{"class":165},[156,904,905],{"class":202},"\"towers\"",[156,907,206],{"class":165},[156,909,910],{"class":202},"\"ogr\"",[156,912,212],{"class":165},[156,914,915,918,920],{"class":75,"line":237},[156,916,917],{"class":165},"paths ",[156,919,196],{"class":161},[156,921,922],{"class":165}," []\n",[156,924,925,927,930,932],{"class":75,"line":242},[156,926,587],{"class":161},[156,928,929],{"class":165}," f ",[156,931,593],{"class":161},[156,933,934],{"class":165}," towers.getFeatures():\n",[156,936,937,940,942],{"class":75,"line":259},[156,938,939],{"class":165},"    p ",[156,941,196],{"class":161},[156,943,944],{"class":165}," f.geometry().asPoint()\n",[156,946,947,950,952,955,958,960,963,966,969,971],{"class":75,"line":268},[156,948,949],{"class":165},"    out ",[156,951,196],{"class":161},[156,953,954],{"class":161}," f",[156,956,957],{"class":202},"\"\u002Fdata\u002Fresults\u002Fvs_",[156,959,297],{"class":226},[156,961,962],{"class":165},"f[",[156,964,965],{"class":202},"'tower_id'",[156,967,968],{"class":165},"]",[156,970,303],{"class":226},[156,972,973],{"class":202},".tif\"\n",[156,975,976,979,981],{"class":75,"line":283},[156,977,978],{"class":165},"    processing.run(",[156,980,253],{"class":202},[156,982,256],{"class":165},[156,984,985,988,991,994,996,998,1000,1003,1005,1007,1009,1011,1014,1016,1018,1020,1023,1025,1027,1029,1031,1033,1035],{"class":75,"line":331},[156,986,987],{"class":202},"        \"INPUT\"",[156,989,990],{"class":165},": dem, ",[156,992,993],{"class":202},"\"BAND\"",[156,995,274],{"class":165},[156,997,277],{"class":226},[156,999,206],{"class":165},[156,1001,1002],{"class":202},"\"OBSERVER\"",[156,1004,274],{"class":165},[156,1006,291],{"class":161},[156,1008,294],{"class":202},[156,1010,297],{"class":226},[156,1012,1013],{"class":165},"p.x()",[156,1015,303],{"class":226},[156,1017,306],{"class":202},[156,1019,297],{"class":226},[156,1021,1022],{"class":165},"p.y()",[156,1024,303],{"class":226},[156,1026,316],{"class":202},[156,1028,297],{"class":226},[156,1030,321],{"class":165},[156,1032,303],{"class":226},[156,1034,326],{"class":202},[156,1036,280],{"class":165},[156,1038,1039,1042,1045,1048,1051,1054,1056,1058,1060,1063,1065,1068],{"class":75,"line":344},[156,1040,1041],{"class":202},"        \"OBSERVER_HEIGHT\"",[156,1043,1044],{"class":165},": f[",[156,1046,1047],{"class":202},"\"height_m\"",[156,1049,1050],{"class":165},"], ",[156,1052,1053],{"class":202},"\"TARGET_HEIGHT\"",[156,1055,274],{"class":165},[156,1057,46],{"class":226},[156,1059,206],{"class":165},[156,1061,1062],{"class":202},"\"MAX_DISTANCE\"",[156,1064,274],{"class":165},[156,1066,1067],{"class":226},"15000",[156,1069,280],{"class":165},[156,1071,1072,1075],{"class":75,"line":357},[156,1073,1074],{"class":202},"        \"OUTPUT\"",[156,1076,1077],{"class":165},": out})\n",[156,1079,1080],{"class":75,"line":370},[156,1081,1082],{"class":165},"    paths.append(out)\n",[156,1084,1085],{"class":75,"line":383},[156,1086,187],{"emptyLinePlaceholder":186},[156,1088,1089,1092,1094],{"class":75,"line":395},[156,1090,1091],{"class":165},"stack ",[156,1093,196],{"class":161},[156,1095,634],{"class":226},[156,1097,1098,1100,1103,1105],{"class":75,"line":400},[156,1099,587],{"class":161},[156,1101,1102],{"class":165}," path ",[156,1104,593],{"class":161},[156,1106,1107],{"class":165}," paths:\n",[156,1109,1110,1113,1115,1118,1121,1124,1127,1130],{"class":75,"line":416},[156,1111,1112],{"class":165},"    arr ",[156,1114,196],{"class":161},[156,1116,1117],{"class":165}," (gdal.Open(path).ReadAsArray() ",[156,1119,1120],{"class":161},">",[156,1122,1123],{"class":226}," 0",[156,1125,1126],{"class":165},").astype(",[156,1128,1129],{"class":202},"\"uint8\"",[156,1131,212],{"class":165},[156,1133,1135,1138,1140,1143,1145,1148,1151,1154,1157,1159,1162],{"class":75,"line":1134},19,[156,1136,1137],{"class":165},"    stack ",[156,1139,196],{"class":161},[156,1141,1142],{"class":165}," arr ",[156,1144,599],{"class":161},[156,1146,1147],{"class":165}," stack ",[156,1149,1150],{"class":161},"is",[156,1152,1153],{"class":226}," None",[156,1155,1156],{"class":161}," else",[156,1158,1147],{"class":165},[156,1160,1161],{"class":161},"+",[156,1163,1164],{"class":165}," arr\n",[156,1166,1168,1170,1172,1175,1177,1180,1183,1185,1187],{"class":75,"line":1167},20,[156,1169,639],{"class":226},[156,1171,642],{"class":165},[156,1173,1174],{"class":202},"\"cells seen by at least one tower:\"",[156,1176,206],{"class":165},[156,1178,1179],{"class":226},"int",[156,1181,1182],{"class":165},"((stack ",[156,1184,1120],{"class":161},[156,1186,1123],{"class":226},[156,1188,1189],{"class":165},").sum()))\n",[14,1191,1192,1194,1195,1198,1199,835],{},[423,1193,425],{}," Each tower's viewshed uses its own height from the attribute table. Converting each output to 0\u002F1 and summing gives the number of towers that see every cell; ",[125,1196,1197],{},"stack > 0"," is the combined coverage, and cells with zero are blind spots. All viewsheds share the DEM's grid, so they align without resampling — as long as the same DEM and the same maximum distance are used. Write the stack back to a GeoTIFF with the DEM's geotransform, as in ",[21,1200,1202],{"href":1201},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fwrite-numpy-array-to-raster-pyqgis\u002F","writing a NumPy array to a raster",[113,1204,1206],{"id":1205},"use-the-right-surface","Use the right surface",[14,1208,1209],{},"The most important decision is not a parameter but the input. A bare-earth DEM has no buildings or trees, so a viewshed on it shows what would be visible on an empty landscape — typically far more than in reality.",[14,1211,1212],{},[29,1213,1216,1219,1222,1225,1228,1232,1237,1241,1245,1249,1253,1256],{"viewBox":1214,"role":32,"ariaLabel":1215,"xmlns":34},"0 0 760 230","A bare-earth DEM overstating visibility through buildings and trees compared with a surface model that includes them",[36,1217,1218],{},"DEM or DSM",[40,1220,1221],{},"A bare-earth digital terrain model omits buildings and trees, so lines of sight pass through them and the viewshed overstates visibility, often greatly in towns and forests. A digital surface model includes them, giving realistic results for observers on the ground. For targets on roofs or in clearings, the DSM's treetops and rooftops are the target surface, which is usually what you want.",[44,1223],{"x":46,"y":46,"width":47,"height":1224,"fill":49},"230",[51,1226,1227],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Bare earth sees through houses",[44,1229],{"x":470,"y":471,"width":1230,"height":1231,"rx":474,"fill":96,"stroke":89,"style":477},"340","150",[51,1233,1236],{"x":1234,"y":1235,"style":482,"fill":89,"textAnchor":57},"194","106.78","DTM (bare earth)",[51,1238,1240],{"x":1234,"y":1239,"style":101,"fill":487,"textAnchor":57},"134.78","no buildings, no trees",[51,1242,1244],{"x":1234,"y":1243,"style":101,"fill":64,"textAnchor":57},"162.78","visibility overstated",[44,1246],{"x":1247,"y":471,"width":1230,"height":1231,"rx":474,"fill":1248,"stroke":79,"style":477},"396","#e8efe6",[51,1250,1252],{"x":1251,"y":1235,"style":482,"fill":110,"textAnchor":57},"566","DSM (surface)",[51,1254,1255],{"x":1251,"y":1239,"style":101,"fill":487,"textAnchor":57},"buildings and canopy",[51,1257,1258],{"x":1251,"y":1243,"style":101,"fill":64,"textAnchor":57},"realistic from the ground",[14,1260,1261,1262,1266],{},"Use a DSM from LiDAR or photogrammetry wherever possible — ",[21,1263,1265],{"href":1264},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fcreate-dem-from-point-cloud-pyqgis\u002F","creating a DEM from a point cloud"," shows how to produce one with all returns. If only a DTM exists, the result is an upper bound: \"at most this is visible\". Resolution matters too: a 25 m DEM smooths away small crests that block views, while a 1 m DSM can make a single hedge block an entire field of view. Choose the resolution of the question, and report which surface was used with every viewshed map.",[113,1268,1270],{"id":1269},"summarise-visibility-for-reports","Summarise visibility for reports",[14,1272,1273],{},"Planning reports need numbers: how many homes see the turbine, how many hectares of the protected area are visible from the new road.",[147,1275,1277],{"className":149,"code":1276,"language":151,"meta":152,"style":152},"from qgis.core import QgsVectorLayer\n\nhomes = QgsVectorLayer(\"\u002Fdata\u002Fsites\u002Fresidential_addresses.gpkg\", \"homes\", \"ogr\")\nsampled = processing.run(\"native:rastersampling\", {\n    \"INPUT\": homes, \"RASTERCOPY\": grass_vs, \"COLUMN_PREFIX\": \"vis_\",\n    \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\nvisible = sum(1 for f in sampled.getFeatures() if f[\"vis_1\"] == 1)\nprint(f\"{visible} of {homes.featureCount()} homes can see the turbine hub\")\n",[125,1278,1279,1289,1293,1316,1330,1353,1368,1410],{"__ignoreMap":152},[156,1280,1281,1283,1285,1287],{"class":75,"line":158},[156,1282,172],{"class":161},[156,1284,175],{"class":165},[156,1286,162],{"class":161},[156,1288,858],{"class":165},[156,1290,1291],{"class":75,"line":169},[156,1292,187],{"emptyLinePlaceholder":186},[156,1294,1295,1298,1300,1302,1305,1307,1310,1312,1314],{"class":75,"line":183},[156,1296,1297],{"class":165},"homes ",[156,1299,196],{"class":161},[156,1301,897],{"class":165},[156,1303,1304],{"class":202},"\"\u002Fdata\u002Fsites\u002Fresidential_addresses.gpkg\"",[156,1306,206],{"class":165},[156,1308,1309],{"class":202},"\"homes\"",[156,1311,206],{"class":165},[156,1313,910],{"class":202},[156,1315,212],{"class":165},[156,1317,1318,1321,1323,1325,1328],{"class":75,"line":190},[156,1319,1320],{"class":165},"sampled ",[156,1322,196],{"class":161},[156,1324,250],{"class":165},[156,1326,1327],{"class":202},"\"native:rastersampling\"",[156,1329,256],{"class":165},[156,1331,1332,1334,1337,1340,1343,1346,1348,1351],{"class":75,"line":215},[156,1333,262],{"class":202},[156,1335,1336],{"class":165},": homes, ",[156,1338,1339],{"class":202},"\"RASTERCOPY\"",[156,1341,1342],{"class":165},": grass_vs, ",[156,1344,1345],{"class":202},"\"COLUMN_PREFIX\"",[156,1347,274],{"class":165},[156,1349,1350],{"class":202},"\"vis_\"",[156,1352,280],{"class":165},[156,1354,1355,1357,1359,1362,1364,1366],{"class":75,"line":237},[156,1356,373],{"class":202},[156,1358,274],{"class":165},[156,1360,1361],{"class":202},"\"memory:\"",[156,1363,386],{"class":165},[156,1365,389],{"class":202},[156,1367,392],{"class":165},[156,1369,1370,1373,1375,1378,1380,1382,1385,1387,1389,1392,1394,1397,1400,1402,1405,1408],{"class":75,"line":242},[156,1371,1372],{"class":165},"visible ",[156,1374,196],{"class":161},[156,1376,1377],{"class":226}," sum",[156,1379,642],{"class":165},[156,1381,277],{"class":226},[156,1383,1384],{"class":161}," for",[156,1386,929],{"class":165},[156,1388,593],{"class":161},[156,1390,1391],{"class":165}," sampled.getFeatures() ",[156,1393,599],{"class":161},[156,1395,1396],{"class":165}," f[",[156,1398,1399],{"class":202},"\"vis_1\"",[156,1401,623],{"class":165},[156,1403,1404],{"class":161},"==",[156,1406,1407],{"class":226}," 1",[156,1409,212],{"class":165},[156,1411,1412,1414,1416,1418,1420,1422,1425,1427,1430,1432,1435,1437,1440],{"class":75,"line":259},[156,1413,639],{"class":226},[156,1415,642],{"class":165},[156,1417,291],{"class":161},[156,1419,294],{"class":202},[156,1421,297],{"class":226},[156,1423,1424],{"class":165},"visible",[156,1426,303],{"class":226},[156,1428,1429],{"class":202}," of ",[156,1431,297],{"class":226},[156,1433,1434],{"class":165},"homes.featureCount()",[156,1436,303],{"class":226},[156,1438,1439],{"class":202}," homes can see the turbine hub\"",[156,1441,212],{"class":165},[14,1443,1444,1446,1447,1451],{},[423,1445,425],{}," Sampling the viewshed at each address turns the raster into a count of affected homes, the figure planning committees ask for. Areas within polygons — protected land, a park — come from zonal statistics of the binary viewshed, as in ",[21,1448,1450],{"href":1449},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis\u002F","zonal statistics",". Always report the observer and target heights, maximum distance, curvature setting and surface used next to the numbers.",[113,1453,1455],{"id":1454},"qgis-version-compatibility","QGIS version compatibility",[14,1457,1458,1460,1461,1463],{},[125,1459,127],{}," requires QGIS 3.28 or newer with GDAL 3.1+, and is present on 3.34 LTR, 3.40 LTR and QGIS 4. The GRASS ",[125,1462,131],{}," algorithm's provider prefix differs between releases; find it by suffix as shown. Parameter names follow GRASS's own option names.",[113,1465,1467],{"id":1466},"troubleshooting","Troubleshooting",[118,1469,1470,1476,1484,1490],{},[121,1471,1472,1475],{},[423,1473,1474],{},"Everything is visible."," The surface is a bare-earth DTM, or target height is far above the ground.",[121,1477,1478,426,1481,1483],{},[423,1479,1480],{},"Nothing beyond a few kilometres is visible.",[125,1482,437],{}," is too small, or curvature is applied at a coarse resolution.",[121,1485,1486,1489],{},[423,1487,1488],{},"The observer cell is invisible."," The observer lies on NoData or outside the DEM.",[121,1491,1492,1495],{},[423,1493,1494],{},"Results differ between GDAL and GRASS."," Curvature, refraction and cell-centre conventions differ; use one tool consistently.",[113,1497,1499],{"id":1498},"conclusion","Conclusion",[14,1501,1502,1503,1505,1506,1508],{},"Run viewsheds with ",[125,1504,127],{}," for quick results and GRASS ",[125,1507,131],{}," when curvature and refraction matter, set observer and target heights to express the real question, bound the distance sensibly, combine observers into cumulative coverage, use a surface model rather than bare earth, and sample results at homes or summarise by area for reports.",[113,1510,1512],{"id":1511},"frequently-asked-questions","Frequently Asked Questions",[14,1514,1515,1518,1519,1523],{},[423,1516,1517],{},"Is a viewshed the same as a line-of-sight profile?","\nA profile tests one line; a viewshed tests every cell. Use ",[21,1520,1522],{"href":1521},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fextract-elevation-profile-along-line-pyqgis\u002F","an elevation profile"," to explain a single result.",[14,1525,1526,1529],{},[423,1527,1528],{},"How long does a viewshed take?","\nSeconds to minutes; time grows with the square of the maximum distance in cells.",[14,1531,1532,1535,1536,1540],{},[423,1533,1534],{},"Can I compute visibility from a line, such as a road?","\nPlace observers along the line, as in ",[21,1537,1539],{"href":1538},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fgenerate-points-along-lines-pyqgis\u002F","generating points along lines",", and build a cumulative viewshed.",[14,1542,1543,1546],{},[423,1544,1545],{},"Should the observer stand on the DSM or the DTM?","\nOn a DSM, an observer placed under a tree canopy stands on the treetop. Move observers to open ground, or sample the DTM for their base elevation and add the height yourself.",[14,1548,1549,1552],{},[423,1550,1551],{},"Can I weight visibility by distance?","\nYes — multiply the binary viewshed by a distance-decay raster built from the observer, so nearby visible areas count more than distant ones.",[14,1554,1555,1558],{},[423,1556,1557],{},"Does the QGIS Visibility Analysis plugin do the same?","\nYes, with additional options such as intervisibility networks; it is a good interactive companion.",[113,1560,1562],{"id":1561},"related","Related",[118,1564,1565,1570,1576,1581,1586],{},[121,1566,1567,1569],{},[21,1568,24],{"href":23}," — the guide this recipe belongs to",[121,1571,1572],{},[21,1573,1575],{"href":1574},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fgenerate-slope-aspect-hillshade-pyqgis\u002F","Generate Slope, Aspect and Hillshade in PyQGIS",[121,1577,1578],{},[21,1579,1580],{"href":1521},"Extract an Elevation Profile Along a Line in PyQGIS",[121,1582,1583],{},[21,1584,1585],{"href":1264},"Create a DEM from a Point Cloud in PyQGIS",[121,1587,1588],{},[21,1589,1590],{"href":833},"Run GRASS and SAGA Algorithms from PyQGIS",[1592,1593,1594],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":152,"searchDepth":169,"depth":169,"links":1596},[1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608],{"id":115,"depth":169,"text":116},{"id":141,"depth":169,"text":142},{"id":445,"depth":169,"text":446},{"id":547,"depth":169,"text":548},{"id":838,"depth":169,"text":839},{"id":1205,"depth":169,"text":1206},{"id":1269,"depth":169,"text":1270},{"id":1454,"depth":169,"text":1455},{"id":1466,"depth":169,"text":1467},{"id":1498,"depth":169,"text":1499},{"id":1511,"depth":169,"text":1512},{"id":1561,"depth":169,"text":1562},"Compute what can be seen from an observer point with gdal:viewshed or GRASS r.viewshed — observer and target heights, maximum distance, earth curvature and refraction, cumulative viewsheds from several points, and why a DEM without buildings or trees overstates visibility.","md",{"slug":1612,"type":1613,"breadcrumb":1614,"datePublished":1615,"dateModified":1615},"run-viewshed-analysis-pyqgis","article","Run a Viewshed Analysis","2026-10-02","\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Frun-viewshed-analysis-pyqgis",{"title":5,"description":1609},"spatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Frun-viewshed-analysis-pyqgis\u002Findex","5D7kgcW9Y2kby0eEPhRJwD2Q1mfObAfE2Wuh6FPDP5g",1790966265137]