[{"data":1,"prerenderedAt":2133},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fdelineate-watersheds-pyqgis":3},{"id":4,"title":5,"body":6,"description":2122,"extension":2123,"meta":2124,"navigation":244,"path":2129,"seo":2130,"stem":2131,"__hash__":2132},"docs\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fdelineate-watersheds-pyqgis\u002Findex.md","Delineate Watersheds in PyQGIS",{"type":7,"value":8,"toc":2107},"minimark",[9,13,17,35,179,184,197,201,204,433,452,456,459,585,597,601,606,665,824,838,842,845,1005,1017,1021,1024,1094,1523,1528,1532,1537,1802,1814,1818,1821,1961,1971,1975,1994,1998,2024,2028,2037,2041,2047,2053,2059,2070,2074,2103],[10,11,5],"h1",{"id":12},"delineate-watersheds-in-pyqgis",[14,15,16],"p",{},"Every point in a landscape drains somewhere. A watershed — or catchment, or drainage basin — is all the land that drains to a given outlet: a gauging station, a culvert, a lake inlet, a bridge. Delineating it from an elevation model is a standard hydrological workflow: fill spurious depressions, compute the direction water flows from each cell, accumulate flow downstream, extract streams, and trace upstream from an outlet. QGIS runs all of it through the GRASS algorithms, and PyQGIS makes it repeatable for any number of outlets.",[14,18,19,20,25,26,30,31,34],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002F","Terrain & Interpolation Analysis",". It conditions a DEM, computes flow accumulation and drainage direction with ",[27,28,29],"code",{},"r.watershed",", extracts a stream network with a chosen threshold, snaps outlets onto streams, delineates basins with ",[27,32,33],{},"r.water.outlet",", polygonizes them and checks their areas.",[14,36,37],{},[38,39,44,48,52,59,76,85,94,100,105,111,116,120,124,128,132,137,141,144,148,154,158,161,165,171,176],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 226","img","Five steps from a DEM to catchment polygons: fill sinks, compute direction and accumulation, extract streams, snap outlets, trace basins","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"From DEM to catchment",[49,50,51],"desc",{},"The workflow runs in five steps. A DEM is conditioned by filling sinks. r.watershed computes flow direction and flow accumulation. Thresholding accumulation gives a stream network. Outlet points are snapped onto the nearest high-accumulation cell. r.water.outlet traces every cell draining to each outlet, giving a basin raster that is polygonized into catchment polygons.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","226","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"wsChainArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Fill, route, threshold, snap, trace",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":67,"fill":91,"stroke":92,"style":93},"24","70","128","140","#fffdf7","#59645f","stroke-width:2",[77,95,99],{"x":96,"y":97,"style":98,"fill":82,"textAnchor":83},"88","131.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","DEM",[77,101,104],{"x":96,"y":102,"style":103,"fill":92,"textAnchor":83},"155.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","fill sinks",[106,107],"line",{"x1":108,"y1":90,"x2":109,"y2":90,"stroke":75,"style":110},"152","176","stroke-width:1.8;marker-end:url(#wsChainArrow)",[53,112],{"x":113,"y":88,"width":90,"height":90,"rx":67,"fill":114,"stroke":115,"style":93},"180","#eef7f4","#0f766e",[77,117,29],{"x":118,"y":119,"style":98,"fill":115,"textAnchor":83},"250","119.78",[77,121,123],{"x":118,"y":122,"style":103,"fill":92,"textAnchor":83},"143.78","direction",[77,125,127],{"x":118,"y":126,"style":103,"fill":92,"textAnchor":83},"167.78","accumulation",[106,129],{"x1":130,"y1":90,"x2":131,"y2":90,"stroke":75,"style":110},"320","344",[53,133],{"x":134,"y":88,"width":89,"height":90,"rx":67,"fill":135,"stroke":136,"style":93},"348","#eff3ff","#2563eb",[77,138,140],{"x":139,"y":97,"style":98,"fill":136,"textAnchor":83},"412","streams",[77,142,143],{"x":139,"y":102,"style":103,"fill":92,"textAnchor":83},"threshold",[106,145],{"x1":146,"y1":90,"x2":147,"y2":90,"stroke":75,"style":110},"476","500",[53,149],{"x":150,"y":88,"width":151,"height":90,"rx":67,"fill":152,"stroke":153,"style":93},"504","112","#fdf2e2","#b45309",[77,155,157],{"x":156,"y":97,"style":98,"fill":153,"textAnchor":83},"560","snap",[77,159,160],{"x":156,"y":102,"style":103,"fill":92,"textAnchor":83},"outlets",[106,162],{"x1":163,"y1":90,"x2":164,"y2":90,"stroke":75,"style":110},"616","640",[53,166],{"x":167,"y":88,"width":168,"height":90,"rx":67,"fill":169,"stroke":170,"style":93},"644","92","#e8efe6","#15803d",[77,172,175],{"x":173,"y":97,"style":98,"fill":174,"textAnchor":83},"690","#166534","basins",[77,177,178],{"x":173,"y":102,"style":103,"fill":92,"textAnchor":83},"polygons",[180,181,183],"h2",{"id":182},"prerequisites","Prerequisites",[185,186,187,191,194],"ul",{},[188,189,190],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, with the GRASS Processing provider (included in the standard Windows and macOS installers; a separate package on some Linux distributions).",[188,192,193],{},"A DEM in a projected CRS with metre units, covering the whole area that drains to your outlets — a catchment cut off by the DEM edge will be truncated.",[188,195,196],{},"Outlet points, such as gauging stations, as a point layer.",[180,198,200],{"id":199},"find-the-grass-algorithms","Find the GRASS algorithms",[14,202,203],{},"The GRASS provider's algorithm ids have a version-dependent prefix. A small helper finds them by name so scripts work on every release.",[205,206,211],"pre",{"className":207,"code":208,"language":209,"meta":210,"style":210},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsApplication\n\ndef grass(name):\n    for alg in QgsApplication.processingRegistry().algorithms():\n        if alg.id().split(\":\")[-1] == name and alg.provider().id().startswith(\"grass\"):\n            return alg.id()\n    raise RuntimeError(f\"GRASS algorithm {name} not available - is the GRASS provider enabled?\")\n\nR_WATERSHED = grass(\"r.watershed\")\nR_OUTLET = grass(\"r.water.outlet\")\nR_FILL = grass(\"r.fill.dir\")\nprint(R_WATERSHED, R_OUTLET, R_FILL)\n","python","",[27,212,213,225,239,246,259,274,318,327,360,365,382,397,412],{"__ignoreMap":210},[214,215,217,221],"span",{"class":106,"line":216},1,[214,218,220],{"class":219},"snl16","import",[214,222,224],{"class":223},"s95oV"," processing\n",[214,226,228,231,234,236],{"class":106,"line":227},2,[214,229,230],{"class":219},"from",[214,232,233],{"class":223}," qgis.core ",[214,235,220],{"class":219},[214,237,238],{"class":223}," QgsApplication\n",[214,240,242],{"class":106,"line":241},3,[214,243,245],{"emptyLinePlaceholder":244},true,"\n",[214,247,249,252,256],{"class":106,"line":248},4,[214,250,251],{"class":219},"def",[214,253,255],{"class":254},"svObZ"," grass",[214,257,258],{"class":223},"(name):\n",[214,260,262,265,268,271],{"class":106,"line":261},5,[214,263,264],{"class":219},"    for",[214,266,267],{"class":223}," alg ",[214,269,270],{"class":219},"in",[214,272,273],{"class":223}," QgsApplication.processingRegistry().algorithms():\n",[214,275,277,280,283,287,290,293,297,300,303,306,309,312,315],{"class":106,"line":276},6,[214,278,279],{"class":219},"        if",[214,281,282],{"class":223}," alg.id().split(",[214,284,286],{"class":285},"sU2Wk","\":\"",[214,288,289],{"class":223},")[",[214,291,292],{"class":219},"-",[214,294,296],{"class":295},"sDLfK","1",[214,298,299],{"class":223},"] ",[214,301,302],{"class":219},"==",[214,304,305],{"class":223}," name ",[214,307,308],{"class":219},"and",[214,310,311],{"class":223}," alg.provider().id().startswith(",[214,313,314],{"class":285},"\"grass\"",[214,316,317],{"class":223},"):\n",[214,319,321,324],{"class":106,"line":320},7,[214,322,323],{"class":219},"            return",[214,325,326],{"class":223}," alg.id()\n",[214,328,330,333,336,339,342,345,348,351,354,357],{"class":106,"line":329},8,[214,331,332],{"class":219},"    raise",[214,334,335],{"class":295}," RuntimeError",[214,337,338],{"class":223},"(",[214,340,341],{"class":219},"f",[214,343,344],{"class":285},"\"GRASS algorithm ",[214,346,347],{"class":295},"{",[214,349,350],{"class":223},"name",[214,352,353],{"class":295},"}",[214,355,356],{"class":285}," not available - is the GRASS provider enabled?\"",[214,358,359],{"class":223},")\n",[214,361,363],{"class":106,"line":362},9,[214,364,245],{"emptyLinePlaceholder":244},[214,366,368,371,374,377,380],{"class":106,"line":367},10,[214,369,370],{"class":295},"R_WATERSHED",[214,372,373],{"class":219}," =",[214,375,376],{"class":223}," grass(",[214,378,379],{"class":285},"\"r.watershed\"",[214,381,359],{"class":223},[214,383,385,388,390,392,395],{"class":106,"line":384},11,[214,386,387],{"class":295},"R_OUTLET",[214,389,373],{"class":219},[214,391,376],{"class":223},[214,393,394],{"class":285},"\"r.water.outlet\"",[214,396,359],{"class":223},[214,398,400,403,405,407,410],{"class":106,"line":399},12,[214,401,402],{"class":295},"R_FILL",[214,404,373],{"class":219},[214,406,376],{"class":223},[214,408,409],{"class":285},"\"r.fill.dir\"",[214,411,359],{"class":223},[214,413,415,418,420,422,425,427,429,431],{"class":106,"line":414},13,[214,416,417],{"class":295},"print",[214,419,338],{"class":223},[214,421,370],{"class":295},[214,423,424],{"class":223},", ",[214,426,387],{"class":295},[214,428,424],{"class":223},[214,430,402],{"class":295},[214,432,359],{"class":223},[14,434,435,439,440,443,444,447,448,451],{},[436,437,438],"strong",{},"Breakdown:"," Matching on the part after the colon finds ",[27,441,442],{},"grass:r.watershed"," or ",[27,445,446],{},"grass7:r.watershed"," alike. Raising a clear error when GRASS is missing saves confusion later — the most common reason these workflows fail on a new machine is a QGIS installed without the GRASS provider. ",[27,449,450],{},"processing.algorithmHelp(R_WATERSHED)"," lists parameter names, which follow GRASS's own option names.",[180,453,455],{"id":454},"condition-the-dem","Condition the DEM",[14,457,458],{},"Real DEMs contain small artificial depressions — from interpolation, from bridges and culverts that the DEM does not represent — where water would pool instead of flowing on. Filling them makes every cell drain to the edge or an outlet.",[205,460,462],{"className":207,"code":461,"language":209,"meta":210,"style":210},"from qgis.core import QgsRasterLayer\n\ndem = \"\u002Fdata\u002Fterrain\u002Fdem_10m.tif\"\nfilled = processing.run(R_FILL, {\n    \"input\": dem,\n    \"format\": 0,\n    \"output\": \"\u002Fdata\u002Fhydro\u002Fdem_filled.tif\",\n    \"direction\": \"\u002Fdata\u002Fhydro\u002Ffill_direction.tif\",\n    \"areas\": \"\u002Fdata\u002Fhydro\u002Fproblem_areas.tif\",\n})[\"output\"]\nprint(\"filled DEM written:\", filled)\n",[27,463,464,475,479,490,505,513,526,538,550,562,573],{"__ignoreMap":210},[214,465,466,468,470,472],{"class":106,"line":216},[214,467,230],{"class":219},[214,469,233],{"class":223},[214,471,220],{"class":219},[214,473,474],{"class":223}," QgsRasterLayer\n",[214,476,477],{"class":106,"line":227},[214,478,245],{"emptyLinePlaceholder":244},[214,480,481,484,487],{"class":106,"line":241},[214,482,483],{"class":223},"dem ",[214,485,486],{"class":219},"=",[214,488,489],{"class":285}," \"\u002Fdata\u002Fterrain\u002Fdem_10m.tif\"\n",[214,491,492,495,497,500,502],{"class":106,"line":248},[214,493,494],{"class":223},"filled ",[214,496,486],{"class":219},[214,498,499],{"class":223}," processing.run(",[214,501,402],{"class":295},[214,503,504],{"class":223},", {\n",[214,506,507,510],{"class":106,"line":261},[214,508,509],{"class":285},"    \"input\"",[214,511,512],{"class":223},": dem,\n",[214,514,515,518,521,523],{"class":106,"line":276},[214,516,517],{"class":285},"    \"format\"",[214,519,520],{"class":223},": ",[214,522,55],{"class":295},[214,524,525],{"class":223},",\n",[214,527,528,531,533,536],{"class":106,"line":320},[214,529,530],{"class":285},"    \"output\"",[214,532,520],{"class":223},[214,534,535],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fdem_filled.tif\"",[214,537,525],{"class":223},[214,539,540,543,545,548],{"class":106,"line":329},[214,541,542],{"class":285},"    \"direction\"",[214,544,520],{"class":223},[214,546,547],{"class":285},"\"\u002Fdata\u002Fhydro\u002Ffill_direction.tif\"",[214,549,525],{"class":223},[214,551,552,555,557,560],{"class":106,"line":362},[214,553,554],{"class":285},"    \"areas\"",[214,556,520],{"class":223},[214,558,559],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fproblem_areas.tif\"",[214,561,525],{"class":223},[214,563,564,567,570],{"class":106,"line":367},[214,565,566],{"class":223},"})[",[214,568,569],{"class":285},"\"output\"",[214,571,572],{"class":223},"]\n",[214,574,575,577,579,582],{"class":106,"line":384},[214,576,417],{"class":295},[214,578,338],{"class":223},[214,580,581],{"class":285},"\"filled DEM written:\"",[214,583,584],{"class":223},", filled)\n",[14,586,587,589,590,593,594,596],{},[436,588,438],{}," ",[27,591,592],{},"r.fill.dir"," raises depression cells to their pour point so flow can continue, and writes the direction raster and a raster of problem areas it could not resolve. Filling is standard for small sinks but wrong for real depressions such as quarries and closed lake basins; check the problem areas and the difference between filled and original DEM where it is large. ",[27,595,29],{}," itself handles depressions with a least-cost search and often works on unfilled DEMs, so compare results with and without filling for your terrain.",[180,598,600],{"id":599},"compute-accumulation-and-drainage","Compute accumulation and drainage",[14,602,603,605],{},[27,604,29],{}," computes, for every cell, the direction water leaves it and how many cells drain through it — flow accumulation, the basis for streams and catchments.",[14,607,608],{},[38,609,612,615,618,620,623,628,631,635,638,642,647,654,660],{"viewBox":610,"role":41,"ariaLabel":611,"xmlns":43},"0 0 760 250","Flow accumulation values growing from ridges to valley bottoms, forming a branching network that a threshold turns into streams",[45,613,614],{},"Flow accumulation reveals streams",[49,616,617],{},"Flow accumulation counts upstream cells. Ridges have values near one; valley bottoms accumulate thousands. Displayed with a logarithmic stretch, accumulation looks like a branching stream network. A threshold, for example cells draining more than one square kilometre, turns it into a stream raster; a smaller threshold gives more and smaller streams.",[53,619],{"x":55,"y":55,"width":56,"height":118,"fill":58},[77,621,622],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Upstream area grows towards the valleys",[72,624],{"d":625,"fill":626,"stroke":136,"style":627},"M 80 60 C 160 100 200 140 260 170","none","stroke-width:1.5",[72,629],{"d":630,"fill":626,"stroke":136,"style":627},"M 120 220 C 180 200 220 190 260 170",[72,632],{"d":633,"fill":626,"stroke":136,"style":634},"M 260 170 C 340 150 420 160 500 150","stroke-width:4",[72,636],{"d":637,"fill":626,"stroke":136,"style":93},"M 420 60 C 450 100 470 130 500 150",[72,639],{"d":640,"fill":626,"stroke":136,"style":641},"M 500 150 C 580 140 640 150 700 160","stroke-width:6",[77,643,646],{"x":644,"y":645,"style":103,"fill":92,"textAnchor":83},"120","52","ridge: ~1 cell",[77,648,653],{"x":649,"y":650,"style":651,"fill":136,"textAnchor":652},"700","190","text-anchor:end;font-size:10.5px;font-family:sans-serif","end","outlet: 10⁶ cells",[53,655],{"x":656,"y":657,"width":658,"height":659,"rx":67,"fill":91,"stroke":92,"style":93},"520","200","216","44",[77,661,664],{"x":662,"y":663,"style":103,"fill":75,"textAnchor":83},"628","225.78","threshold → streams",[205,666,668],{"className":207,"code":667,"language":209,"meta":210,"style":210},"ws = processing.run(R_WATERSHED, {\n    \"elevation\": filled,\n    \"threshold\": 10000,              # cells; 10,000 × 100 m² = 1 km²\n    \"-s\": True,                      # single flow direction (D8)\n    \"-a\": True,                      # positive accumulation only\n    \"accumulation\": \"\u002Fdata\u002Fhydro\u002Faccumulation.tif\",\n    \"drainage\": \"\u002Fdata\u002Fhydro\u002Fdrainage.tif\",\n    \"stream\": \"\u002Fdata\u002Fhydro\u002Fstreams.tif\",\n    \"basin\": \"\u002Fdata\u002Fhydro\u002Fsubbasins.tif\",\n})\nprint({k: v for k, v in ws.items() if isinstance(v, str)})\n",[27,669,670,683,691,708,724,738,750,762,774,786,791],{"__ignoreMap":210},[214,671,672,675,677,679,681],{"class":106,"line":216},[214,673,674],{"class":223},"ws ",[214,676,486],{"class":219},[214,678,499],{"class":223},[214,680,370],{"class":295},[214,682,504],{"class":223},[214,684,685,688],{"class":106,"line":227},[214,686,687],{"class":285},"    \"elevation\"",[214,689,690],{"class":223},": filled,\n",[214,692,693,696,698,701,704],{"class":106,"line":241},[214,694,695],{"class":285},"    \"threshold\"",[214,697,520],{"class":223},[214,699,700],{"class":295},"10000",[214,702,703],{"class":223},",              ",[214,705,707],{"class":706},"sjoCn","# cells; 10,000 × 100 m² = 1 km²\n",[214,709,710,713,715,718,721],{"class":106,"line":248},[214,711,712],{"class":285},"    \"-s\"",[214,714,520],{"class":223},[214,716,717],{"class":295},"True",[214,719,720],{"class":223},",                      ",[214,722,723],{"class":706},"# single flow direction (D8)\n",[214,725,726,729,731,733,735],{"class":106,"line":261},[214,727,728],{"class":285},"    \"-a\"",[214,730,520],{"class":223},[214,732,717],{"class":295},[214,734,720],{"class":223},[214,736,737],{"class":706},"# positive accumulation only\n",[214,739,740,743,745,748],{"class":106,"line":276},[214,741,742],{"class":285},"    \"accumulation\"",[214,744,520],{"class":223},[214,746,747],{"class":285},"\"\u002Fdata\u002Fhydro\u002Faccumulation.tif\"",[214,749,525],{"class":223},[214,751,752,755,757,760],{"class":106,"line":320},[214,753,754],{"class":285},"    \"drainage\"",[214,756,520],{"class":223},[214,758,759],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fdrainage.tif\"",[214,761,525],{"class":223},[214,763,764,767,769,772],{"class":106,"line":329},[214,765,766],{"class":285},"    \"stream\"",[214,768,520],{"class":223},[214,770,771],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fstreams.tif\"",[214,773,525],{"class":223},[214,775,776,779,781,784],{"class":106,"line":362},[214,777,778],{"class":285},"    \"basin\"",[214,780,520],{"class":223},[214,782,783],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fsubbasins.tif\"",[214,785,525],{"class":223},[214,787,788],{"class":106,"line":367},[214,789,790],{"class":223},"})\n",[214,792,793,795,798,801,804,806,809,812,815,818,821],{"class":106,"line":384},[214,794,417],{"class":295},[214,796,797],{"class":223},"({k: v ",[214,799,800],{"class":219},"for",[214,802,803],{"class":223}," k, v ",[214,805,270],{"class":219},[214,807,808],{"class":223}," ws.items() ",[214,810,811],{"class":219},"if",[214,813,814],{"class":295}," isinstance",[214,816,817],{"class":223},"(v, ",[214,819,820],{"class":295},"str",[214,822,823],{"class":223},")})\n",[14,825,826,589,828,830,831,834,835,837],{},[436,827,438],{},[27,829,143],{}," is the minimum drainage area, in cells, for a stream and for subbasin generation; at 10 m resolution, 10,000 cells is 1 km². Single flow direction (",[27,832,833],{},"-s",") sends all flow to the steepest neighbour, which gives clean, connected streams; the default multiple-flow algorithm spreads flow and suits hillslope hydrology better. The drainage raster encodes direction and is what ",[27,836,33],{}," needs. Subbasins delineated at the threshold are a useful by-product: every stream segment's own catchment.",[180,839,841],{"id":840},"choose-a-stream-threshold","Choose a stream threshold",[14,843,844],{},"The threshold decides how dense the stream network is. Too small and every gully is a stream; too large and real streams are missed. Comparing against a mapped river network is the best calibration.",[205,846,848],{"className":207,"code":847,"language":209,"meta":210,"style":210},"for km2 in (0.25, 1, 4):\n    cells = int(km2 * 1e6 \u002F 100)            # 10 m cells\n    processing.run(R_WATERSHED, {\n        \"elevation\": filled, \"threshold\": cells, \"-s\": True, \"-a\": True,\n        \"stream\": f\"\u002Fdata\u002Fhydro\u002Fstreams_{km2}km2.tif\"})\n    print(f\"threshold {km2} km² = {cells} cells\")\n",[27,849,850,876,907,916,948,972],{"__ignoreMap":210},[214,851,852,854,857,859,862,865,867,869,871,874],{"class":106,"line":216},[214,853,800],{"class":219},[214,855,856],{"class":223}," km2 ",[214,858,270],{"class":219},[214,860,861],{"class":223}," (",[214,863,864],{"class":295},"0.25",[214,866,424],{"class":223},[214,868,296],{"class":295},[214,870,424],{"class":223},[214,872,873],{"class":295},"4",[214,875,317],{"class":223},[214,877,878,881,883,886,889,892,895,898,901,904],{"class":106,"line":227},[214,879,880],{"class":223},"    cells ",[214,882,486],{"class":219},[214,884,885],{"class":295}," int",[214,887,888],{"class":223},"(km2 ",[214,890,891],{"class":219},"*",[214,893,894],{"class":295}," 1e6",[214,896,897],{"class":219}," \u002F",[214,899,900],{"class":295}," 100",[214,902,903],{"class":223},")            ",[214,905,906],{"class":706},"# 10 m cells\n",[214,908,909,912,914],{"class":106,"line":241},[214,910,911],{"class":223},"    processing.run(",[214,913,370],{"class":295},[214,915,504],{"class":223},[214,917,918,921,924,927,930,933,935,937,939,942,944,946],{"class":106,"line":248},[214,919,920],{"class":285},"        \"elevation\"",[214,922,923],{"class":223},": filled, ",[214,925,926],{"class":285},"\"threshold\"",[214,928,929],{"class":223},": cells, ",[214,931,932],{"class":285},"\"-s\"",[214,934,520],{"class":223},[214,936,717],{"class":295},[214,938,424],{"class":223},[214,940,941],{"class":285},"\"-a\"",[214,943,520],{"class":223},[214,945,717],{"class":295},[214,947,525],{"class":223},[214,949,950,953,955,957,960,962,965,967,970],{"class":106,"line":261},[214,951,952],{"class":285},"        \"stream\"",[214,954,520],{"class":223},[214,956,341],{"class":219},[214,958,959],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fstreams_",[214,961,347],{"class":295},[214,963,964],{"class":223},"km2",[214,966,353],{"class":295},[214,968,969],{"class":285},"km2.tif\"",[214,971,790],{"class":223},[214,973,974,977,979,981,984,986,988,990,993,995,998,1000,1003],{"class":106,"line":276},[214,975,976],{"class":295},"    print",[214,978,338],{"class":223},[214,980,341],{"class":219},[214,982,983],{"class":285},"\"threshold ",[214,985,347],{"class":295},[214,987,964],{"class":223},[214,989,353],{"class":295},[214,991,992],{"class":285}," km² = ",[214,994,347],{"class":295},[214,996,997],{"class":223},"cells",[214,999,353],{"class":295},[214,1001,1002],{"class":285}," cells\"",[214,1004,359],{"class":223},[14,1006,1007,1009,1010,443,1013,1016],{},[436,1008,438],{}," Running a few thresholds and overlaying each stream raster on mapped rivers shows which matches the official network's density; that threshold is then the right one for this landscape. Steep, wet terrain supports channels at small drainage areas; flat or dry terrain needs larger ones. Convert the chosen stream raster to lines with ",[27,1011,1012],{},"r.to.vect",[27,1014,1015],{},"r.thin"," plus polygonizing for mapping.",[180,1018,1020],{"id":1019},"snap-outlets-to-the-stream-network","Snap outlets to the stream network",[14,1022,1023],{},"An outlet point that lies a few metres off the computed stream sits on a hillslope, and its \"catchment\" is a tiny patch. Moving each outlet to the highest-accumulation cell nearby fixes it.",[14,1025,1026],{},[38,1027,1030,1033,1036,1039,1046,1049,1055,1060,1064,1068,1071,1076,1080,1084,1088],{"viewBox":1028,"role":41,"ariaLabel":1029,"xmlns":43},"0 0 760 230","An outlet point moved from a hillslope cell to the nearby cell with the highest flow accumulation before delineation",[45,1031,1032],{},"Snapping an outlet onto the stream",[49,1034,1035],{},"A gauging station recorded beside the river lies on a hillslope cell with small accumulation. Searching a small radius around it for the cell with the highest accumulation moves the outlet onto the computed stream. Without snapping the delineated catchment covers a few cells; with it, the whole upstream basin.",[53,1037],{"x":55,"y":55,"width":56,"height":1038,"fill":58},"230",[60,1040,1041],{},[63,1042,1044],{"id":1043,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"wsSnapArrow",[72,1045],{"d":74,"fill":75},[77,1047,1048],{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"Outlets belong on the computed stream",[53,1050],{"x":87,"y":1051,"width":1052,"height":1053,"rx":67,"fill":152,"stroke":1054,"style":93},"56","340","150","#b91c1c",[77,1056,1059],{"x":1057,"y":1058,"style":98,"fill":1054,"textAnchor":83},"194","106.78","unsnapped",[77,1061,1063],{"x":1057,"y":1062,"style":103,"fill":75,"textAnchor":83},"134.78","outlet on hillslope",[77,1065,1067],{"x":1057,"y":1066,"style":103,"fill":92,"textAnchor":83},"162.78","catchment: a few cells",[53,1069],{"x":1070,"y":1051,"width":1052,"height":1053,"rx":67,"fill":169,"stroke":170,"style":93},"396",[77,1072,1075],{"x":1073,"y":1074,"style":98,"fill":174,"textAnchor":83},"566","92.78","snapped",[77,1077,1079],{"x":1073,"y":1078,"style":103,"fill":75,"textAnchor":83},"120.78","max accumulation",[77,1081,1083],{"x":1073,"y":1082,"style":103,"fill":75,"textAnchor":83},"148.78","within 50 m",[77,1085,1087],{"x":1073,"y":1086,"style":103,"fill":92,"textAnchor":83},"176.78","catchment: whole basin",[106,1089],{"x1":1090,"y1":1091,"x2":1092,"y2":1091,"stroke":75,"style":1093},"364","131","392","stroke-width:1.8;marker-end:url(#wsSnapArrow)",[205,1095,1097],{"className":207,"code":1096,"language":209,"meta":210,"style":210},"import numpy as np\nfrom osgeo import gdal\nfrom qgis.core import QgsVectorLayer, QgsPointXY\n\nacc_ds = gdal.Open(\"\u002Fdata\u002Fhydro\u002Faccumulation.tif\")\nacc = acc_ds.ReadAsArray()\ngt = acc_ds.GetGeoTransform()\n\ndef snap(x, y, radius_m=50):\n    col, row = int((x - gt[0]) \u002F gt[1]), int((y - gt[3]) \u002F gt[5])\n    r = int(radius_m \u002F gt[1])\n    window = acc[max(row - r, 0):row + r + 1, max(col - r, 0):col + r + 1]\n    i, j = np.unravel_index(np.argmax(window), window.shape)\n    row2, col2 = max(row - r, 0) + i, max(col - r, 0) + j\n    return gt[0] + (col2 + 0.5) * gt[1], gt[3] + (row2 + 0.5) * gt[5]\n\ngauges = QgsVectorLayer(\"\u002Fdata\u002Fhydro\u002Fgauges.gpkg\", \"gauges\", \"ogr\")\nsnapped = {f[\"gauge_id\"]: snap(*f.geometry().asPoint()) for f in gauges.getFeatures()}\nprint(list(snapped.items())[:3])\n",[27,1098,1099,1112,1124,1135,1139,1153,1163,1173,1177,1194,1250,1270,1333,1343,1387,1443,1448,1474,1506],{"__ignoreMap":210},[214,1100,1101,1103,1106,1109],{"class":106,"line":216},[214,1102,220],{"class":219},[214,1104,1105],{"class":223}," numpy ",[214,1107,1108],{"class":219},"as",[214,1110,1111],{"class":223}," np\n",[214,1113,1114,1116,1119,1121],{"class":106,"line":227},[214,1115,230],{"class":219},[214,1117,1118],{"class":223}," osgeo ",[214,1120,220],{"class":219},[214,1122,1123],{"class":223}," gdal\n",[214,1125,1126,1128,1130,1132],{"class":106,"line":241},[214,1127,230],{"class":219},[214,1129,233],{"class":223},[214,1131,220],{"class":219},[214,1133,1134],{"class":223}," QgsVectorLayer, QgsPointXY\n",[214,1136,1137],{"class":106,"line":248},[214,1138,245],{"emptyLinePlaceholder":244},[214,1140,1141,1144,1146,1149,1151],{"class":106,"line":261},[214,1142,1143],{"class":223},"acc_ds ",[214,1145,486],{"class":219},[214,1147,1148],{"class":223}," gdal.Open(",[214,1150,747],{"class":285},[214,1152,359],{"class":223},[214,1154,1155,1158,1160],{"class":106,"line":276},[214,1156,1157],{"class":223},"acc ",[214,1159,486],{"class":219},[214,1161,1162],{"class":223}," acc_ds.ReadAsArray()\n",[214,1164,1165,1168,1170],{"class":106,"line":320},[214,1166,1167],{"class":223},"gt ",[214,1169,486],{"class":219},[214,1171,1172],{"class":223}," acc_ds.GetGeoTransform()\n",[214,1174,1175],{"class":106,"line":329},[214,1176,245],{"emptyLinePlaceholder":244},[214,1178,1179,1181,1184,1187,1189,1192],{"class":106,"line":362},[214,1180,251],{"class":219},[214,1182,1183],{"class":254}," snap",[214,1185,1186],{"class":223},"(x, y, radius_m",[214,1188,486],{"class":219},[214,1190,1191],{"class":295},"50",[214,1193,317],{"class":223},[214,1195,1196,1199,1201,1203,1206,1208,1211,1213,1216,1219,1221,1223,1226,1229,1232,1234,1236,1239,1241,1243,1245,1247],{"class":106,"line":367},[214,1197,1198],{"class":223},"    col, row ",[214,1200,486],{"class":219},[214,1202,885],{"class":295},[214,1204,1205],{"class":223},"((x ",[214,1207,292],{"class":219},[214,1209,1210],{"class":223}," gt[",[214,1212,55],{"class":295},[214,1214,1215],{"class":223},"]) ",[214,1217,1218],{"class":219},"\u002F",[214,1220,1210],{"class":223},[214,1222,296],{"class":295},[214,1224,1225],{"class":223},"]), ",[214,1227,1228],{"class":295},"int",[214,1230,1231],{"class":223},"((y ",[214,1233,292],{"class":219},[214,1235,1210],{"class":223},[214,1237,1238],{"class":295},"3",[214,1240,1215],{"class":223},[214,1242,1218],{"class":219},[214,1244,1210],{"class":223},[214,1246,68],{"class":295},[214,1248,1249],{"class":223},"])\n",[214,1251,1252,1255,1257,1259,1262,1264,1266,1268],{"class":106,"line":384},[214,1253,1254],{"class":223},"    r ",[214,1256,486],{"class":219},[214,1258,885],{"class":295},[214,1260,1261],{"class":223},"(radius_m ",[214,1263,1218],{"class":219},[214,1265,1210],{"class":223},[214,1267,296],{"class":295},[214,1269,1249],{"class":223},[214,1271,1272,1275,1277,1280,1283,1286,1288,1291,1293,1296,1299,1302,1304,1307,1309,1311,1314,1316,1318,1320,1323,1325,1327,1329,1331],{"class":106,"line":399},[214,1273,1274],{"class":223},"    window ",[214,1276,486],{"class":219},[214,1278,1279],{"class":223}," acc[",[214,1281,1282],{"class":295},"max",[214,1284,1285],{"class":223},"(row ",[214,1287,292],{"class":219},[214,1289,1290],{"class":223}," r, ",[214,1292,55],{"class":295},[214,1294,1295],{"class":223},"):row ",[214,1297,1298],{"class":219},"+",[214,1300,1301],{"class":223}," r ",[214,1303,1298],{"class":219},[214,1305,1306],{"class":295}," 1",[214,1308,424],{"class":223},[214,1310,1282],{"class":295},[214,1312,1313],{"class":223},"(col ",[214,1315,292],{"class":219},[214,1317,1290],{"class":223},[214,1319,55],{"class":295},[214,1321,1322],{"class":223},"):col ",[214,1324,1298],{"class":219},[214,1326,1301],{"class":223},[214,1328,1298],{"class":219},[214,1330,1306],{"class":295},[214,1332,572],{"class":223},[214,1334,1335,1338,1340],{"class":106,"line":414},[214,1336,1337],{"class":223},"    i, j ",[214,1339,486],{"class":219},[214,1341,1342],{"class":223}," np.unravel_index(np.argmax(window), window.shape)\n",[214,1344,1346,1349,1351,1354,1356,1358,1360,1362,1365,1367,1370,1372,1374,1376,1378,1380,1382,1384],{"class":106,"line":1345},14,[214,1347,1348],{"class":223},"    row2, col2 ",[214,1350,486],{"class":219},[214,1352,1353],{"class":295}," max",[214,1355,1285],{"class":223},[214,1357,292],{"class":219},[214,1359,1290],{"class":223},[214,1361,55],{"class":295},[214,1363,1364],{"class":223},") ",[214,1366,1298],{"class":219},[214,1368,1369],{"class":223}," i, ",[214,1371,1282],{"class":295},[214,1373,1313],{"class":223},[214,1375,292],{"class":219},[214,1377,1290],{"class":223},[214,1379,55],{"class":295},[214,1381,1364],{"class":223},[214,1383,1298],{"class":219},[214,1385,1386],{"class":223}," j\n",[214,1388,1390,1393,1395,1397,1399,1401,1404,1406,1409,1411,1413,1415,1417,1420,1422,1424,1426,1429,1431,1433,1435,1437,1439,1441],{"class":106,"line":1389},15,[214,1391,1392],{"class":219},"    return",[214,1394,1210],{"class":223},[214,1396,55],{"class":295},[214,1398,299],{"class":223},[214,1400,1298],{"class":219},[214,1402,1403],{"class":223}," (col2 ",[214,1405,1298],{"class":219},[214,1407,1408],{"class":295}," 0.5",[214,1410,1364],{"class":223},[214,1412,891],{"class":219},[214,1414,1210],{"class":223},[214,1416,296],{"class":295},[214,1418,1419],{"class":223},"], gt[",[214,1421,1238],{"class":295},[214,1423,299],{"class":223},[214,1425,1298],{"class":219},[214,1427,1428],{"class":223}," (row2 ",[214,1430,1298],{"class":219},[214,1432,1408],{"class":295},[214,1434,1364],{"class":223},[214,1436,891],{"class":219},[214,1438,1210],{"class":223},[214,1440,68],{"class":295},[214,1442,572],{"class":223},[214,1444,1446],{"class":106,"line":1445},16,[214,1447,245],{"emptyLinePlaceholder":244},[214,1449,1451,1454,1456,1459,1462,1464,1467,1469,1472],{"class":106,"line":1450},17,[214,1452,1453],{"class":223},"gauges ",[214,1455,486],{"class":219},[214,1457,1458],{"class":223}," QgsVectorLayer(",[214,1460,1461],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fgauges.gpkg\"",[214,1463,424],{"class":223},[214,1465,1466],{"class":285},"\"gauges\"",[214,1468,424],{"class":223},[214,1470,1471],{"class":285},"\"ogr\"",[214,1473,359],{"class":223},[214,1475,1477,1480,1482,1485,1488,1491,1493,1496,1498,1501,1503],{"class":106,"line":1476},18,[214,1478,1479],{"class":223},"snapped ",[214,1481,486],{"class":219},[214,1483,1484],{"class":223}," {f[",[214,1486,1487],{"class":285},"\"gauge_id\"",[214,1489,1490],{"class":223},"]: snap(",[214,1492,891],{"class":219},[214,1494,1495],{"class":223},"f.geometry().asPoint()) ",[214,1497,800],{"class":219},[214,1499,1500],{"class":223}," f ",[214,1502,270],{"class":219},[214,1504,1505],{"class":223}," gauges.getFeatures()}\n",[214,1507,1509,1511,1513,1516,1519,1521],{"class":106,"line":1508},19,[214,1510,417],{"class":295},[214,1512,338],{"class":223},[214,1514,1515],{"class":295},"list",[214,1517,1518],{"class":223},"(snapped.items())[:",[214,1520,1238],{"class":295},[214,1522,1249],{"class":223},[14,1524,1525,1527],{},[436,1526,438],{}," Converting the outlet's coordinates to a row and column, searching a small window and taking the cell with the largest accumulation moves the outlet onto the main channel. The radius should be small — tens of metres — or an outlet near a confluence may jump to the larger river. Returning cell-centre coordinates avoids edge ambiguity. Check snapped outlets against the stream raster on the map before delineating.",[180,1529,1531],{"id":1530},"delineate-and-polygonize-basins","Delineate and polygonize basins",[14,1533,1534,1536],{},[27,1535,33],{}," traces every cell that drains to an outlet. Running it per outlet and polygonizing gives one catchment polygon per gauge.",[205,1538,1540],{"className":207,"code":1539,"language":209,"meta":210,"style":210},"results = []\nfor gid, (x, y) in snapped.items():\n    basin = processing.run(R_OUTLET, {\n        \"input\": \"\u002Fdata\u002Fhydro\u002Fdrainage.tif\",\n        \"coordinates\": f\"{x},{y}\",\n        \"output\": f\"\u002Fdata\u002Fhydro\u002Fbasin_{gid}.tif\"})[\"output\"]\n    poly = processing.run(\"gdal:polygonize\", {\n        \"INPUT\": basin, \"BAND\": 1, \"FIELD\": \"basin\", \"EIGHT_CONNECTEDNESS\": False,\n        \"OUTPUT\": \"memory:\"})[\"OUTPUT\"]\n    for f in poly.getFeatures():\n        if f[\"basin\"] == 1:\n            results.append((gid, f.geometry().area() \u002F 1e6))\nfor gid, km2 in results:\n    print(f\"{gid}: {km2:,.1f} km²\")\n",[27,1541,1542,1552,1564,1577,1588,1621,1649,1663,1700,1717,1728,1746,1758,1770],{"__ignoreMap":210},[214,1543,1544,1547,1549],{"class":106,"line":216},[214,1545,1546],{"class":223},"results ",[214,1548,486],{"class":219},[214,1550,1551],{"class":223}," []\n",[214,1553,1554,1556,1559,1561],{"class":106,"line":227},[214,1555,800],{"class":219},[214,1557,1558],{"class":223}," gid, (x, y) ",[214,1560,270],{"class":219},[214,1562,1563],{"class":223}," snapped.items():\n",[214,1565,1566,1569,1571,1573,1575],{"class":106,"line":241},[214,1567,1568],{"class":223},"    basin ",[214,1570,486],{"class":219},[214,1572,499],{"class":223},[214,1574,387],{"class":295},[214,1576,504],{"class":223},[214,1578,1579,1582,1584,1586],{"class":106,"line":248},[214,1580,1581],{"class":285},"        \"input\"",[214,1583,520],{"class":223},[214,1585,759],{"class":285},[214,1587,525],{"class":223},[214,1589,1590,1593,1595,1597,1600,1602,1605,1607,1610,1612,1615,1617,1619],{"class":106,"line":261},[214,1591,1592],{"class":285},"        \"coordinates\"",[214,1594,520],{"class":223},[214,1596,341],{"class":219},[214,1598,1599],{"class":285},"\"",[214,1601,347],{"class":295},[214,1603,1604],{"class":223},"x",[214,1606,353],{"class":295},[214,1608,1609],{"class":285},",",[214,1611,347],{"class":295},[214,1613,1614],{"class":223},"y",[214,1616,353],{"class":295},[214,1618,1599],{"class":285},[214,1620,525],{"class":223},[214,1622,1623,1626,1628,1630,1633,1635,1638,1640,1643,1645,1647],{"class":106,"line":276},[214,1624,1625],{"class":285},"        \"output\"",[214,1627,520],{"class":223},[214,1629,341],{"class":219},[214,1631,1632],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fbasin_",[214,1634,347],{"class":295},[214,1636,1637],{"class":223},"gid",[214,1639,353],{"class":295},[214,1641,1642],{"class":285},".tif\"",[214,1644,566],{"class":223},[214,1646,569],{"class":285},[214,1648,572],{"class":223},[214,1650,1651,1654,1656,1658,1661],{"class":106,"line":320},[214,1652,1653],{"class":223},"    poly ",[214,1655,486],{"class":219},[214,1657,499],{"class":223},[214,1659,1660],{"class":285},"\"gdal:polygonize\"",[214,1662,504],{"class":223},[214,1664,1665,1668,1671,1674,1676,1678,1680,1683,1685,1688,1690,1693,1695,1698],{"class":106,"line":329},[214,1666,1667],{"class":285},"        \"INPUT\"",[214,1669,1670],{"class":223},": basin, ",[214,1672,1673],{"class":285},"\"BAND\"",[214,1675,520],{"class":223},[214,1677,296],{"class":295},[214,1679,424],{"class":223},[214,1681,1682],{"class":285},"\"FIELD\"",[214,1684,520],{"class":223},[214,1686,1687],{"class":285},"\"basin\"",[214,1689,424],{"class":223},[214,1691,1692],{"class":285},"\"EIGHT_CONNECTEDNESS\"",[214,1694,520],{"class":223},[214,1696,1697],{"class":295},"False",[214,1699,525],{"class":223},[214,1701,1702,1705,1707,1710,1712,1715],{"class":106,"line":362},[214,1703,1704],{"class":285},"        \"OUTPUT\"",[214,1706,520],{"class":223},[214,1708,1709],{"class":285},"\"memory:\"",[214,1711,566],{"class":223},[214,1713,1714],{"class":285},"\"OUTPUT\"",[214,1716,572],{"class":223},[214,1718,1719,1721,1723,1725],{"class":106,"line":367},[214,1720,264],{"class":219},[214,1722,1500],{"class":223},[214,1724,270],{"class":219},[214,1726,1727],{"class":223}," poly.getFeatures():\n",[214,1729,1730,1732,1735,1737,1739,1741,1743],{"class":106,"line":384},[214,1731,279],{"class":219},[214,1733,1734],{"class":223}," f[",[214,1736,1687],{"class":285},[214,1738,299],{"class":223},[214,1740,302],{"class":219},[214,1742,1306],{"class":295},[214,1744,1745],{"class":223},":\n",[214,1747,1748,1751,1753,1755],{"class":106,"line":399},[214,1749,1750],{"class":223},"            results.append((gid, f.geometry().area() ",[214,1752,1218],{"class":219},[214,1754,894],{"class":295},[214,1756,1757],{"class":223},"))\n",[214,1759,1760,1762,1765,1767],{"class":106,"line":414},[214,1761,800],{"class":219},[214,1763,1764],{"class":223}," gid, km2 ",[214,1766,270],{"class":219},[214,1768,1769],{"class":223}," results:\n",[214,1771,1772,1774,1776,1778,1780,1782,1784,1786,1788,1790,1792,1795,1797,1800],{"class":106,"line":1345},[214,1773,976],{"class":295},[214,1775,338],{"class":223},[214,1777,341],{"class":219},[214,1779,1599],{"class":285},[214,1781,347],{"class":295},[214,1783,1637],{"class":223},[214,1785,353],{"class":295},[214,1787,520],{"class":285},[214,1789,347],{"class":295},[214,1791,964],{"class":223},[214,1793,1794],{"class":219},":,.1f",[214,1796,353],{"class":295},[214,1798,1799],{"class":285}," km²\"",[214,1801,359],{"class":223},[14,1803,1804,589,1806,1808,1809,1813],{},[436,1805,438],{},[27,1807,33],{}," writes 1 for cells draining to the outlet and NoData elsewhere; polygonizing and keeping the polygon with value 1 gives the catchment outline. The area in square kilometres is the first check: gauging stations publish their catchment areas, and a delineated area within a few percent confirms the workflow; a large difference points to a misplaced outlet, a DEM edge cutting the basin, or flat areas where flow direction is ambiguous. The ",[21,1810,1812],{"href":1811},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fpolygonize-raster-to-vector-pyqgis\u002F","polygonize recipe"," covers cleaning staircase outlines.",[180,1815,1817],{"id":1816},"summarise-each-catchment","Summarise each catchment",[14,1819,1820],{},"A catchment polygon becomes useful when it carries numbers: mean slope, land-cover shares, rainfall, the length of streams inside it. Zonal statistics over the basins add them in one step per raster.",[205,1822,1824],{"className":207,"code":1823,"language":209,"meta":210,"style":210},"basins = QgsVectorLayer(\"\u002Fdata\u002Fhydro\u002Fcatchments.gpkg\", \"catchments\", \"ogr\")\nfor raster, prefix in ((\"\u002Fdata\u002Fterrain\u002Fslope_deg.tif\", \"slope_\"),\n                       (\"\u002Fdata\u002Fclimate\u002Frain_mm.tif\", \"rain_\")):\n    processing.run(\"native:zonalstatisticsfb\", {\n        \"INPUT\": basins, \"INPUT_RASTER\": raster, \"RASTER_BAND\": 1,\n        \"COLUMN_PREFIX\": prefix, \"STATISTICS\": [2, 4, 6],     # mean, std dev, max\n        \"OUTPUT\": \"memory:\"})\n",[27,1825,1826,1849,1872,1888,1897,1919,1951],{"__ignoreMap":210},[214,1827,1828,1831,1833,1835,1838,1840,1843,1845,1847],{"class":106,"line":216},[214,1829,1830],{"class":223},"basins ",[214,1832,486],{"class":219},[214,1834,1458],{"class":223},[214,1836,1837],{"class":285},"\"\u002Fdata\u002Fhydro\u002Fcatchments.gpkg\"",[214,1839,424],{"class":223},[214,1841,1842],{"class":285},"\"catchments\"",[214,1844,424],{"class":223},[214,1846,1471],{"class":285},[214,1848,359],{"class":223},[214,1850,1851,1853,1856,1858,1861,1864,1866,1869],{"class":106,"line":227},[214,1852,800],{"class":219},[214,1854,1855],{"class":223}," raster, prefix ",[214,1857,270],{"class":219},[214,1859,1860],{"class":223}," ((",[214,1862,1863],{"class":285},"\"\u002Fdata\u002Fterrain\u002Fslope_deg.tif\"",[214,1865,424],{"class":223},[214,1867,1868],{"class":285},"\"slope_\"",[214,1870,1871],{"class":223},"),\n",[214,1873,1874,1877,1880,1882,1885],{"class":106,"line":241},[214,1875,1876],{"class":223},"                       (",[214,1878,1879],{"class":285},"\"\u002Fdata\u002Fclimate\u002Frain_mm.tif\"",[214,1881,424],{"class":223},[214,1883,1884],{"class":285},"\"rain_\"",[214,1886,1887],{"class":223},")):\n",[214,1889,1890,1892,1895],{"class":106,"line":248},[214,1891,911],{"class":223},[214,1893,1894],{"class":285},"\"native:zonalstatisticsfb\"",[214,1896,504],{"class":223},[214,1898,1899,1901,1904,1907,1910,1913,1915,1917],{"class":106,"line":261},[214,1900,1667],{"class":285},[214,1902,1903],{"class":223},": basins, ",[214,1905,1906],{"class":285},"\"INPUT_RASTER\"",[214,1908,1909],{"class":223},": raster, ",[214,1911,1912],{"class":285},"\"RASTER_BAND\"",[214,1914,520],{"class":223},[214,1916,296],{"class":295},[214,1918,525],{"class":223},[214,1920,1921,1924,1927,1930,1933,1936,1938,1940,1942,1945,1948],{"class":106,"line":276},[214,1922,1923],{"class":285},"        \"COLUMN_PREFIX\"",[214,1925,1926],{"class":223},": prefix, ",[214,1928,1929],{"class":285},"\"STATISTICS\"",[214,1931,1932],{"class":223},": [",[214,1934,1935],{"class":295},"2",[214,1937,424],{"class":223},[214,1939,873],{"class":295},[214,1941,424],{"class":223},[214,1943,1944],{"class":295},"6",[214,1946,1947],{"class":223},"],     ",[214,1949,1950],{"class":706},"# mean, std dev, max\n",[214,1952,1953,1955,1957,1959],{"class":106,"line":320},[214,1954,1704],{"class":285},[214,1956,520],{"class":223},[214,1958,1709],{"class":285},[214,1960,790],{"class":223},[14,1962,1963,1965,1966,1970],{},[436,1964,438],{}," The feature-based zonal statistics algorithm adds the chosen statistics as fields with the given prefix — mean, standard deviation and maximum here, by their positions in the algorithm's statistics list. Mean slope and rainfall per catchment are classic inputs for runoff estimates and for comparing gauged with ungauged basins. Writing the outputs to a GeoPackage alongside the catchment polygons gives one table describing every basin; ",[21,1967,1969],{"href":1968},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis\u002F","zonal statistics"," covers statistics codes and performance.",[180,1972,1974],{"id":1973},"qgis-version-compatibility","QGIS version compatibility",[14,1976,1977,424,1979,1981,1982,1984,1985,424,1988,1981,1990,1993],{},[27,1978,29],{},[27,1980,33],{}," and ",[27,1983,592],{}," are available through the GRASS provider on QGIS 3.34 LTR, 3.40 LTR and QGIS 4 when GRASS is installed; the provider prefix varies, hence the lookup helper. GRASS option names such as ",[27,1986,1987],{},"elevation",[27,1989,143],{},[27,1991,1992],{},"drainage"," follow GRASS 8.",[180,1995,1997],{"id":1996},"troubleshooting","Troubleshooting",[185,1999,2000,2006,2012,2018],{},[188,2001,2002,2005],{},[436,2003,2004],{},"The catchment is tiny."," The outlet is off the stream; snap it to high accumulation.",[188,2007,2008,2011],{},[436,2009,2010],{},"The catchment is cut by a straight line."," The DEM does not cover the whole basin; extend it upstream.",[188,2013,2014,2017],{},[436,2015,2016],{},"Streams appear in flat floodplains as parallel lines."," Flow direction is ambiguous on flat areas; burn in mapped streams or use a coarser threshold.",[188,2019,2020,2023],{},[436,2021,2022],{},"GRASS algorithms are missing."," The GRASS provider is not installed or not enabled.",[180,2025,2027],{"id":2026},"conclusion","Conclusion",[14,2029,2030,2031,2033,2034,2036],{},"Find GRASS algorithms by name, fill spurious sinks while checking real depressions, compute accumulation and drainage with ",[27,2032,29],{},", calibrate the stream threshold against mapped rivers, snap outlets onto the computed streams, trace and polygonize basins with ",[27,2035,33],{},", and check areas against published catchment sizes.",[180,2038,2040],{"id":2039},"frequently-asked-questions","Frequently Asked Questions",[14,2042,2043,2046],{},[436,2044,2045],{},"Can I do this without GRASS?","\nSAGA and WhiteboxTools offer equivalent tools through their Processing providers; the steps are the same.",[14,2048,2049,2052],{},[436,2050,2051],{},"What resolution should the DEM have?","\nFine enough to resolve the channels that matter — 10–25 m for regional catchments, 1–2 m for urban drainage.",[14,2054,2055,2058],{},[436,2056,2057],{},"How do I handle culverts under roads?","\nBurn them into the DEM by lowering cells along the culvert line before computing flow.",[14,2060,2061,2064,2065,1981,2067,2069],{},[436,2062,2063],{},"Can I get the stream network as lines?","\nYes — thin the stream raster and convert to vector with ",[27,2066,1015],{},[27,2068,1012],{},".",[180,2071,2073],{"id":2072},"related","Related",[185,2075,2076,2081,2087,2093,2098],{},[188,2077,2078,2080],{},[21,2079,24],{"href":23}," — the guide this recipe belongs to",[188,2082,2083],{},[21,2084,2086],{"href":2085},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-grass-and-saga-algorithms-pyqgis\u002F","Run GRASS and SAGA Algorithms from PyQGIS",[188,2088,2089],{},[21,2090,2092],{"href":2091},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fgenerate-slope-aspect-hillshade-pyqgis\u002F","Generate Slope, Aspect and Hillshade in PyQGIS",[188,2094,2095],{},[21,2096,2097],{"href":1811},"Polygonize a Raster to Vector in PyQGIS",[188,2099,2100],{},[21,2101,2102],{"href":1968},"Zonal Statistics in PyQGIS",[2104,2105,2106],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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":210,"searchDepth":227,"depth":227,"links":2108},[2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121],{"id":182,"depth":227,"text":183},{"id":199,"depth":227,"text":200},{"id":454,"depth":227,"text":455},{"id":599,"depth":227,"text":600},{"id":840,"depth":227,"text":841},{"id":1019,"depth":227,"text":1020},{"id":1530,"depth":227,"text":1531},{"id":1816,"depth":227,"text":1817},{"id":1973,"depth":227,"text":1974},{"id":1996,"depth":227,"text":1997},{"id":2026,"depth":227,"text":2027},{"id":2039,"depth":227,"text":2040},{"id":2072,"depth":227,"text":2073},"Derive flow direction, flow accumulation, stream networks and catchments from a DEM with GRASS r.watershed and r.water.outlet run from PyQGIS — conditioning the DEM, choosing a stream threshold, snapping outlets to streams, polygonizing basins and checking areas against known catchments.","md",{"slug":2125,"type":2126,"breadcrumb":2127,"datePublished":2128,"dateModified":2128},"delineate-watersheds-pyqgis","article","Delineate Watersheds","2026-10-02","\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fdelineate-watersheds-pyqgis",{"title":5,"description":2122},"spatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fdelineate-watersheds-pyqgis\u002Findex","MyWYs247z6qlqlWMm-qP_GyP5uzegBy8fsI20yDusg8",1790966265052]