[{"data":1,"prerenderedAt":1613},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fcreate-contours-from-dem-pyqgis":3},{"id":4,"title":5,"body":6,"description":1602,"extension":1603,"meta":1604,"navigation":232,"path":1609,"seo":1610,"stem":1611,"__hash__":1612},"docs\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fcreate-contours-from-dem-pyqgis\u002Findex.md","Create Contours from a DEM in PyQGIS",{"type":7,"value":8,"toc":1588},"minimark",[9,13,17,26,173,178,195,199,206,384,409,420,424,427,430,527,537,541,544,749,767,770,832,836,839,842,903,912,915,987,992,996,999,1209,1238,1242,1245,1329,1348,1352,1358,1445,1449,1496,1500,1512,1516,1529,1535,1546,1552,1556,1584],[10,11,5],"h1",{"id":12},"create-contours-from-a-dem-in-pyqgis",[14,15,16],"p",{},"Contours are the most readable terrain product ever invented and the easiest to get wrong. The algorithm call is one line; the decisions around it — what interval, how to handle a DEM that steps in whole metres, which lines to label — are what separate a map somebody can navigate with from a mat of tangled spaghetti.",[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 in PyQGIS",". It covers producing lines and filled bands, choosing an interval against the terrain rather than for roundness, dealing with terracing in coarse or integer DEMs, and setting up index contours with labels that follow the curve.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,81,99,105,109,113,117,122,127,131,144,152,159,165,168],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 320","img","A cross-section through terrain with horizontal planes at a fixed vertical interval, and the resulting contour lines in plan view showing lines close together on steep ground and far apart on gentle ground","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Contours are horizontal slices through the surface",[40,41,42],"desc",{},"In cross-section, evenly spaced horizontal planes intersect the terrain surface. Each intersection becomes one contour line. Where the ground is steep the intersections are close together horizontally, so the plan view shows tightly packed lines; where the ground is gentle the same vertical interval produces widely spaced lines.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","320","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","A fixed vertical interval, a varying horizontal spacing",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","52","350","228","10","#fffdf7","#59645f","stroke-width:2",[51,70,74],{"x":71,"y":72,"style":73,"fill":56,"textAnchor":57},"199","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","cross-section",[76,77],"path",{"d":78,"fill":79,"stroke":56,"style":80},"M48 250 L110 236 L160 176 L210 120 L250 108 L300 168 L350 244","#e7e2d4","stroke-width:2.5",[82,83,86,90,93,96],"g",{"stroke":84,"style":85},"#2563eb","stroke-width:1.4;stroke-dasharray:5 4",[87,88],"line",{"x1":89,"y1":64,"x2":63,"y2":64},"48",[87,91],{"x1":89,"y1":92,"x2":63,"y2":92},"196",[87,94],{"x1":89,"y1":95,"x2":63,"y2":95},"164",[87,97],{"x1":89,"y1":98,"x2":63,"y2":98},"132",[51,100,104],{"x":101,"y":102,"style":103,"fill":84},"368","232","text-anchor:end;font-size:9.5px;font-family:sans-serif","110",[51,106,108],{"x":101,"y":107,"style":103,"fill":84},"200","120",[51,110,112],{"x":101,"y":111,"style":103,"fill":84},"168","130",[51,114,116],{"x":101,"y":115,"style":103,"fill":84},"136","140",[51,118,121],{"x":71,"y":119,"style":120,"fill":67,"textAnchor":57},"272","text-anchor:middle;font-size:10.5px;font-family:sans-serif","planes every 10 m",[44,123],{"x":124,"y":62,"width":125,"height":64,"rx":65,"fill":66,"stroke":126,"style":80},"398","338","#0f766e",[51,128,130],{"x":129,"y":72,"style":73,"fill":126,"textAnchor":57},"567","plan view",[82,132,135,138,141],{"fill":133,"stroke":126,"style":134},"none","stroke-width:1.6",[76,136],{"d":137},"M424 244 Q500 200 470 140 Q450 100 520 90 Q600 84 640 130",[76,139],{"d":140},"M436 250 Q516 208 486 146 Q464 100 522 98 Q606 92 650 138",[76,142],{"d":143},"M448 256 Q532 216 502 152 Q478 102 524 106 Q612 100 660 146",[82,145,146,149],{"fill":133,"stroke":126,"style":134},[76,147],{"d":148},"M560 258 Q600 236 660 240",[76,150],{"d":151},"M556 274 Q604 254 676 258",[51,153,158],{"x":154,"y":155,"style":156,"fill":157},"486","122","font-size:9.5px;font-family:sans-serif","#b45309","steep",[51,160,164],{"x":161,"y":162,"style":156,"fill":163},"606","278","#15803d","gentle",[51,166,167],{"x":129,"y":119,"style":120,"fill":67,"textAnchor":57},"spacing encodes gradient",[51,169,172],{"x":53,"y":170,"style":171,"fill":67,"textAnchor":57},"306","text-anchor:middle;font-size:11px;font-family:sans-serif","choose the interval so the tightest ground stays readable at the output scale",[174,175,177],"h2",{"id":176},"prerequisites","Prerequisites",[179,180,181,189,192],"ul",{},[182,183,184,188],"li",{},[185,186,187],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[182,190,191],{},"A DEM with nodata declared. Undeclared fill values produce contour rings around every gap at absurd elevations.",[182,193,194],{},"A projected CRS is not strictly required for contouring — the vertical values are read directly — but it is required for anything you do with the result, and for the smoothing step below.",[174,196,198],{"id":197},"generate-the-lines","Generate the lines",[14,200,201,205],{},[202,203,204],"code",{},"gdal:contour"," is the workhorse and takes a fixed interval.",[207,208,213],"pre",{"className":209,"code":210,"language":211,"meta":212,"style":212},"language-python shiki shiki-themes github-dark","import processing\n\nprocessing.run(\"gdal:contour\", {\n    \"INPUT\": \"\u002Fdata\u002Fdem_27700.tif\",\n    \"BAND\": 1,\n    \"INTERVAL\": 10.0,\n    \"FIELD_NAME\": \"elev\",\n    \"BASE\": 0.0,\n    \"CREATE_3D\": False,\n    \"IGNORE_NODATA\": False,\n    \"NODATA\": None,\n    \"OFFSET\": 0.0,\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fcontours_10m.gpkg\",\n})\n","python","",[202,214,215,227,234,247,262,276,289,302,315,328,340,353,365,378],{"__ignoreMap":212},[216,217,219,223],"span",{"class":87,"line":218},1,[216,220,222],{"class":221},"snl16","import",[216,224,226],{"class":225},"s95oV"," processing\n",[216,228,230],{"class":87,"line":229},2,[216,231,233],{"emptyLinePlaceholder":232},true,"\n",[216,235,237,240,244],{"class":87,"line":236},3,[216,238,239],{"class":225},"processing.run(",[216,241,243],{"class":242},"sU2Wk","\"gdal:contour\"",[216,245,246],{"class":225},", {\n",[216,248,250,253,256,259],{"class":87,"line":249},4,[216,251,252],{"class":242},"    \"INPUT\"",[216,254,255],{"class":225},": ",[216,257,258],{"class":242},"\"\u002Fdata\u002Fdem_27700.tif\"",[216,260,261],{"class":225},",\n",[216,263,265,268,270,274],{"class":87,"line":264},5,[216,266,267],{"class":242},"    \"BAND\"",[216,269,255],{"class":225},[216,271,273],{"class":272},"sDLfK","1",[216,275,261],{"class":225},[216,277,279,282,284,287],{"class":87,"line":278},6,[216,280,281],{"class":242},"    \"INTERVAL\"",[216,283,255],{"class":225},[216,285,286],{"class":272},"10.0",[216,288,261],{"class":225},[216,290,292,295,297,300],{"class":87,"line":291},7,[216,293,294],{"class":242},"    \"FIELD_NAME\"",[216,296,255],{"class":225},[216,298,299],{"class":242},"\"elev\"",[216,301,261],{"class":225},[216,303,305,308,310,313],{"class":87,"line":304},8,[216,306,307],{"class":242},"    \"BASE\"",[216,309,255],{"class":225},[216,311,312],{"class":272},"0.0",[216,314,261],{"class":225},[216,316,318,321,323,326],{"class":87,"line":317},9,[216,319,320],{"class":242},"    \"CREATE_3D\"",[216,322,255],{"class":225},[216,324,325],{"class":272},"False",[216,327,261],{"class":225},[216,329,331,334,336,338],{"class":87,"line":330},10,[216,332,333],{"class":242},"    \"IGNORE_NODATA\"",[216,335,255],{"class":225},[216,337,325],{"class":272},[216,339,261],{"class":225},[216,341,343,346,348,351],{"class":87,"line":342},11,[216,344,345],{"class":242},"    \"NODATA\"",[216,347,255],{"class":225},[216,349,350],{"class":272},"None",[216,352,261],{"class":225},[216,354,356,359,361,363],{"class":87,"line":355},12,[216,357,358],{"class":242},"    \"OFFSET\"",[216,360,255],{"class":225},[216,362,312],{"class":272},[216,364,261],{"class":225},[216,366,368,371,373,376],{"class":87,"line":367},13,[216,369,370],{"class":242},"    \"OUTPUT\"",[216,372,255],{"class":225},[216,374,375],{"class":242},"\"\u002Fdata\u002Foutput\u002Fcontours_10m.gpkg\"",[216,377,261],{"class":225},[216,379,381],{"class":87,"line":380},14,[216,382,383],{"class":225},"})\n",[14,385,386,389,390,393,394,397,398,401,402,404,405,408],{},[185,387,388],{},"Breakdown:"," ",[202,391,392],{},"FIELD_NAME"," writes the elevation of each line into an attribute, and skipping it produces a layer of unlabelled, unstyleable lines — always set it. ",[202,395,396],{},"BASE"," anchors the sequence, so a base of 0 with an interval of 10 yields lines at 0, 10, 20; a base of 5 yields 5, 15, 25, which is occasionally what a local convention requires. ",[202,399,400],{},"IGNORE_NODATA"," set to ",[202,403,325],{}," is the safe default: with it ",[202,406,407],{},"True",", GDAL treats gaps as interpolable and draws contours across them.",[14,410,411,414,415,419],{},[202,412,413],{},"CREATE_3D"," writes the elevation into the geometry's Z coordinate as well as the attribute. It is worth turning on when the contours will feed a ",[21,416,418],{"href":417},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fbuild-tin-interpolation-pyqgis\u002F","TIN interpolation"," or a 3D view, and worth leaving off otherwise, because a Z-aware layer is fussier to edit and some downstream algorithms drop the dimension silently.",[174,421,423],{"id":422},"choosing-the-interval","Choosing the interval",[14,425,426],{},"There is no correct interval, only intervals that suit the terrain and the output scale.",[14,428,429],{},"The usable rule is a limit on line density: contours closer than about half a millimetre on the printed page merge into a solid tone. At 1:25 000, half a millimetre is 12.5 metres on the ground, so on a slope of 30 degrees — about 0.58 metres of rise per metre of run — the closest acceptable interval is around 7 metres. Round to 10 and the steepest ground stays readable.",[207,431,433],{"className":209,"code":432,"language":211,"meta":212,"style":212},"def suggest_interval(map_scale, max_slope_degrees, min_mm=0.5):\n    import math\n    ground_mm = map_scale * min_mm \u002F 1000.0\n    return ground_mm * math.tan(math.radians(max_slope_degrees))\n\nprint(suggest_interval(25000, 30))     # ≈ 7.2 metres\n",[202,434,435,456,464,486,499,503],{"__ignoreMap":212},[216,436,437,440,444,447,450,453],{"class":87,"line":218},[216,438,439],{"class":221},"def",[216,441,443],{"class":442},"svObZ"," suggest_interval",[216,445,446],{"class":225},"(map_scale, max_slope_degrees, min_mm",[216,448,449],{"class":221},"=",[216,451,452],{"class":272},"0.5",[216,454,455],{"class":225},"):\n",[216,457,458,461],{"class":87,"line":229},[216,459,460],{"class":221},"    import",[216,462,463],{"class":225}," math\n",[216,465,466,469,471,474,477,480,483],{"class":87,"line":236},[216,467,468],{"class":225},"    ground_mm ",[216,470,449],{"class":221},[216,472,473],{"class":225}," map_scale ",[216,475,476],{"class":221},"*",[216,478,479],{"class":225}," min_mm ",[216,481,482],{"class":221},"\u002F",[216,484,485],{"class":272}," 1000.0\n",[216,487,488,491,494,496],{"class":87,"line":249},[216,489,490],{"class":221},"    return",[216,492,493],{"class":225}," ground_mm ",[216,495,476],{"class":221},[216,497,498],{"class":225}," math.tan(math.radians(max_slope_degrees))\n",[216,500,501],{"class":87,"line":264},[216,502,233],{"emptyLinePlaceholder":232},[216,504,505,508,511,514,517,520,523],{"class":87,"line":278},[216,506,507],{"class":272},"print",[216,509,510],{"class":225},"(suggest_interval(",[216,512,513],{"class":272},"25000",[216,515,516],{"class":225},", ",[216,518,519],{"class":272},"30",[216,521,522],{"class":225},"))     ",[216,524,526],{"class":525},"sjoCn","# ≈ 7.2 metres\n",[14,528,529,531,532,536],{},[185,530,388],{}," The function converts the minimum printable separation into a ground distance, then multiplies by the gradient to get the vertical interval that produces exactly that separation on the steepest ground. It is a floor rather than a recommendation — round up to a conventional number, and remember that the steepest slope in the DEM is often a data artefact rather than real terrain, so use a high percentile from ",[21,533,535],{"href":534},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fzonal-statistics-pyqgis\u002F","zonal statistics"," rather than the maximum.",[174,538,540],{"id":539},"terracing-and-how-to-remove-it-honestly","Terracing, and how to remove it honestly",[14,542,543],{},"A DEM stored as integers, or resampled from a coarser source, has flat steps. Contouring it produces contours that hug the step edges as angular, blocky lines that follow the pixel grid rather than the terrain.",[207,545,547],{"className":209,"code":546,"language":211,"meta":212,"style":212},"smoothed = processing.run(\"native:cellstatistics\", {\n    \"INPUT\": [\"\u002Fdata\u002Fdem_27700.tif\"],\n    \"STATISTIC\": 2,\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})[\"OUTPUT\"]\n\nfiltered = processing.run(\"grass7:r.neighbors\", {\n    \"input\": \"\u002Fdata\u002Fdem_27700.tif\",\n    \"size\": 3,\n    \"method\": 0,                       # average\n    \"output\": \"TEMPORARY_OUTPUT\",\n})[\"output\"]\n\nprocessing.run(\"gdal:contour\", {\n    \"INPUT\": filtered, \"BAND\": 1, \"INTERVAL\": 10.0,\n    \"FIELD_NAME\": \"elev\", \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fcontours_smooth.gpkg\",\n})\n",[202,548,549,564,576,588,599,610,614,628,639,651,666,677,686,690,698,724,744],{"__ignoreMap":212},[216,550,551,554,556,559,562],{"class":87,"line":218},[216,552,553],{"class":225},"smoothed ",[216,555,449],{"class":221},[216,557,558],{"class":225}," processing.run(",[216,560,561],{"class":242},"\"native:cellstatistics\"",[216,563,246],{"class":225},[216,565,566,568,571,573],{"class":87,"line":229},[216,567,252],{"class":242},[216,569,570],{"class":225},": [",[216,572,258],{"class":242},[216,574,575],{"class":225},"],\n",[216,577,578,581,583,586],{"class":87,"line":236},[216,579,580],{"class":242},"    \"STATISTIC\"",[216,582,255],{"class":225},[216,584,585],{"class":272},"2",[216,587,261],{"class":225},[216,589,590,592,594,597],{"class":87,"line":249},[216,591,370],{"class":242},[216,593,255],{"class":225},[216,595,596],{"class":242},"\"TEMPORARY_OUTPUT\"",[216,598,261],{"class":225},[216,600,601,604,607],{"class":87,"line":264},[216,602,603],{"class":225},"})[",[216,605,606],{"class":242},"\"OUTPUT\"",[216,608,609],{"class":225},"]\n",[216,611,612],{"class":87,"line":278},[216,613,233],{"emptyLinePlaceholder":232},[216,615,616,619,621,623,626],{"class":87,"line":291},[216,617,618],{"class":225},"filtered ",[216,620,449],{"class":221},[216,622,558],{"class":225},[216,624,625],{"class":242},"\"grass7:r.neighbors\"",[216,627,246],{"class":225},[216,629,630,633,635,637],{"class":87,"line":304},[216,631,632],{"class":242},"    \"input\"",[216,634,255],{"class":225},[216,636,258],{"class":242},[216,638,261],{"class":225},[216,640,641,644,646,649],{"class":87,"line":317},[216,642,643],{"class":242},"    \"size\"",[216,645,255],{"class":225},[216,647,648],{"class":272},"3",[216,650,261],{"class":225},[216,652,653,656,658,660,663],{"class":87,"line":330},[216,654,655],{"class":242},"    \"method\"",[216,657,255],{"class":225},[216,659,46],{"class":272},[216,661,662],{"class":225},",                       ",[216,664,665],{"class":525},"# average\n",[216,667,668,671,673,675],{"class":87,"line":342},[216,669,670],{"class":242},"    \"output\"",[216,672,255],{"class":225},[216,674,596],{"class":242},[216,676,261],{"class":225},[216,678,679,681,684],{"class":87,"line":355},[216,680,603],{"class":225},[216,682,683],{"class":242},"\"output\"",[216,685,609],{"class":225},[216,687,688],{"class":87,"line":367},[216,689,233],{"emptyLinePlaceholder":232},[216,691,692,694,696],{"class":87,"line":380},[216,693,239],{"class":225},[216,695,243],{"class":242},[216,697,246],{"class":225},[216,699,701,703,706,709,711,713,715,718,720,722],{"class":87,"line":700},15,[216,702,252],{"class":242},[216,704,705],{"class":225},": filtered, ",[216,707,708],{"class":242},"\"BAND\"",[216,710,255],{"class":225},[216,712,273],{"class":272},[216,714,516],{"class":225},[216,716,717],{"class":242},"\"INTERVAL\"",[216,719,255],{"class":225},[216,721,286],{"class":272},[216,723,261],{"class":225},[216,725,727,729,731,733,735,737,739,742],{"class":87,"line":726},16,[216,728,294],{"class":242},[216,730,255],{"class":225},[216,732,299],{"class":242},[216,734,516],{"class":225},[216,736,606],{"class":242},[216,738,255],{"class":225},[216,740,741],{"class":242},"\"\u002Fdata\u002Foutput\u002Fcontours_smooth.gpkg\"",[216,743,261],{"class":225},[216,745,747],{"class":87,"line":746},17,[216,748,383],{"class":225},[14,750,751,753,754,758,759,762,763,766],{},[185,752,388],{}," Smoothing the ",[755,756,757],"em",{},"raster"," before contouring is the honest fix, because the resulting lines still describe a real surface — just a slightly generalised one. Smoothing the ",[755,760,761],{},"lines"," afterwards with ",[202,764,765],{},"native:smoothgeometry"," looks similar and is not equivalent: the smoothed line no longer sits at the elevation its attribute claims, so a point taken from it and sampled against the DEM disagrees. Use line smoothing only for purely decorative output where nobody will measure anything.",[14,768,769],{},"A 3×3 average is a gentle filter. If terracing survives it, the DEM's vertical resolution is genuinely coarser than the contour interval you have chosen, and the correct response is a larger interval rather than heavier smoothing.",[14,771,772],{},[29,773,776,779,782,785,788,793,798,802,805,808,812,816,820,823,826,829],{"viewBox":774,"role":32,"ariaLabel":775,"xmlns":34},"0 0 760 288","Contours from an integer DEM showing blocky terraced lines, compared with contours from the same DEM after a three by three average filter showing smooth curves",[36,777,778],{},"Terracing comes from the raster, so the fix belongs there",[40,780,781],{},"Contouring an integer valued DEM produces angular lines that follow the pixel grid. Applying a small averaging filter to the raster first, then contouring, produces smooth lines that still describe a real surface. Smoothing the lines afterwards would look similar but the lines would no longer match their stated elevations.",[44,783],{"x":46,"y":46,"width":47,"height":784,"fill":49},"288",[51,786,787],{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"Smooth the surface, not the lines",[44,789],{"x":790,"y":62,"width":791,"height":92,"rx":65,"fill":792,"stroke":157,"style":80},"26","330","#fdf2e2",[51,794,797],{"x":795,"y":796,"style":73,"fill":157,"textAnchor":57},"191","78","integer DEM, contoured directly",[76,799],{"d":800,"fill":133,"stroke":157,"style":801},"M60 220 H90 V200 H120 V180 H160 V160 H210 V148 H260 V132 H320","stroke-width:2.2",[76,803],{"d":804,"fill":133,"stroke":157,"style":801},"M60 236 H100 V216 H136 V196 H176 V176 H226 V164 H276 V150 H320",[76,806],{"d":807,"fill":133,"stroke":157,"style":801},"M60 204 H82 V184 H108 V164 H146 V144 H196 V132 H244 V116 H320",[51,809,811],{"x":795,"y":108,"style":120,"fill":810,"textAnchor":57},"#2f3b35","steps follow the pixel grid",[44,813],{"x":814,"y":62,"width":791,"height":92,"rx":65,"fill":815,"stroke":163,"style":80},"404","#edf8e9",[51,817,819],{"x":818,"y":796,"style":73,"fill":163,"textAnchor":57},"569","3×3 average, then contoured",[76,821],{"d":822,"fill":133,"stroke":163,"style":801},"M436 220 Q520 190 600 162 T700 132",[76,824],{"d":825,"fill":133,"stroke":163,"style":801},"M436 236 Q528 208 606 180 T700 150",[76,827],{"d":828,"fill":133,"stroke":163,"style":801},"M436 204 Q512 172 592 144 T700 116",[51,830,831],{"x":818,"y":108,"style":120,"fill":810,"textAnchor":57},"lines still match their elevations",[174,833,835],{"id":834},"cleaning-up-the-output","Cleaning up the output",[14,837,838],{},"Raw contour output usually needs two passes before it is presentable, and both are cheap.",[14,840,841],{},"Short fragments accumulate wherever the surface just grazes a level — a handful of vertices describing a bump two metres across, which at map scale is a dot. Filtering by length removes them without touching anything real.",[207,843,845],{"className":209,"code":844,"language":211,"meta":212,"style":212},"import processing\n\nprocessing.run(\"native:extractbyexpression\", {\n    \"INPUT\": \"\u002Fdata\u002Foutput\u002Fcontours_10m.gpkg\",\n    \"EXPRESSION\": \"$length > 40\",\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fcontours_clean.gpkg\",\n})\n",[202,846,847,853,857,866,876,888,899],{"__ignoreMap":212},[216,848,849,851],{"class":87,"line":218},[216,850,222],{"class":221},[216,852,226],{"class":225},[216,854,855],{"class":87,"line":229},[216,856,233],{"emptyLinePlaceholder":232},[216,858,859,861,864],{"class":87,"line":236},[216,860,239],{"class":225},[216,862,863],{"class":242},"\"native:extractbyexpression\"",[216,865,246],{"class":225},[216,867,868,870,872,874],{"class":87,"line":249},[216,869,252],{"class":242},[216,871,255],{"class":225},[216,873,375],{"class":242},[216,875,261],{"class":225},[216,877,878,881,883,886],{"class":87,"line":264},[216,879,880],{"class":242},"    \"EXPRESSION\"",[216,882,255],{"class":225},[216,884,885],{"class":242},"\"$length > 40\"",[216,887,261],{"class":225},[216,889,890,892,894,897],{"class":87,"line":278},[216,891,370],{"class":242},[216,893,255],{"class":225},[216,895,896],{"class":242},"\"\u002Fdata\u002Foutput\u002Fcontours_clean.gpkg\"",[216,898,261],{"class":225},[216,900,901],{"class":87,"line":291},[216,902,383],{"class":225},[14,904,905,907,908,911],{},[185,906,388],{}," The threshold is a map-scale judgement rather than a terrain one: at 1:25 000, forty metres is about 1.6 mm on the page, which is roughly the shortest line worth drawing. Using ",[202,909,910],{},"$length"," rather than a geometry function keeps the expression readable and lets it be tuned in the interface before being written into the script.",[14,913,914],{},"The second pass is ordering. Contours come out of GDAL in the order the algorithm found them, which means the drawing order is arbitrary and index contours can end up underneath ordinary ones where they cross. Sorting by elevation on export fixes it, and costs nothing.",[207,916,918],{"className":209,"code":917,"language":211,"meta":212,"style":212},"processing.run(\"native:orderbyexpression\", {\n    \"INPUT\": \"\u002Fdata\u002Foutput\u002Fcontours_clean.gpkg\",\n    \"EXPRESSION\": '\"elev\"',\n    \"ASCENDING\": True,\n    \"NULLS_FIRST\": False,\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fcontours_final.gpkg\",\n})\n",[202,919,920,929,939,950,961,972,983],{"__ignoreMap":212},[216,921,922,924,927],{"class":87,"line":218},[216,923,239],{"class":225},[216,925,926],{"class":242},"\"native:orderbyexpression\"",[216,928,246],{"class":225},[216,930,931,933,935,937],{"class":87,"line":229},[216,932,252],{"class":242},[216,934,255],{"class":225},[216,936,896],{"class":242},[216,938,261],{"class":225},[216,940,941,943,945,948],{"class":87,"line":236},[216,942,880],{"class":242},[216,944,255],{"class":225},[216,946,947],{"class":242},"'\"elev\"'",[216,949,261],{"class":225},[216,951,952,955,957,959],{"class":87,"line":249},[216,953,954],{"class":242},"    \"ASCENDING\"",[216,956,255],{"class":225},[216,958,407],{"class":272},[216,960,261],{"class":225},[216,962,963,966,968,970],{"class":87,"line":264},[216,964,965],{"class":242},"    \"NULLS_FIRST\"",[216,967,255],{"class":225},[216,969,325],{"class":272},[216,971,261],{"class":225},[216,973,974,976,978,981],{"class":87,"line":278},[216,975,370],{"class":242},[216,977,255],{"class":225},[216,979,980],{"class":242},"\"\u002Fdata\u002Foutput\u002Fcontours_final.gpkg\"",[216,982,261],{"class":225},[216,984,985],{"class":87,"line":291},[216,986,383],{"class":225},[14,988,989,991],{},[185,990,388],{}," Ordering a layer by an attribute fixes draw order for renderers that respect feature order, which includes the ordinary single-symbol and rule-based renderers. It is not a substitute for a symbol-level rendering pass when two classes genuinely must be drawn in separate sweeps, but for contours — where all the lines are the same kind of thing at different heights — it is exactly the right tool.",[174,993,995],{"id":994},"index-contours-and-labels-that-follow-the-line","Index contours and labels that follow the line",[14,997,998],{},"Every fifth contour drawn heavier and labelled is the convention that makes a contour map readable at a glance. With the elevation in an attribute it is a rule-based renderer plus a label filter.",[207,1000,1002],{"className":209,"code":1001,"language":211,"meta":212,"style":212},"from qgis.core import (\n    QgsVectorLayer, QgsProject, QgsPalLayerSettings, QgsProperty,\n    QgsLabeling, QgsVectorLayerSimpleLabeling,\n)\n\ncontours = QgsVectorLayer(\"\u002Fdata\u002Foutput\u002Fcontours_10m.gpkg\", \"contours\", \"ogr\")\nQgsProject.instance().addMapLayer(contours)\n\nsettings = QgsPalLayerSettings()\nsettings.fieldName = \"elev\"\nsettings.placement = QgsPalLayerSettings.Line\nsettings.lineSettings().setPlacementFlags(\n    QgsLabeling.OnLine | QgsLabeling.MapOrientation\n)\nsettings.isExpression = False\nsettings.obstacle = False\n\nfilter_settings = settings\nfilter_settings.dataDefinedProperties().setProperty(\n    QgsPalLayerSettings.Show,\n    QgsProperty.fromExpression('\"elev\" % 50 = 0'),\n)\n\ncontours.setLabeling(QgsVectorLayerSimpleLabeling(filter_settings))\ncontours.setLabelsEnabled(True)\ncontours.triggerRepaint()\n",[202,1003,1004,1017,1022,1027,1032,1036,1060,1065,1069,1079,1089,1099,1104,1115,1119,1129,1138,1142,1153,1159,1165,1177,1182,1187,1193,1203],{"__ignoreMap":212},[216,1005,1006,1009,1012,1014],{"class":87,"line":218},[216,1007,1008],{"class":221},"from",[216,1010,1011],{"class":225}," qgis.core ",[216,1013,222],{"class":221},[216,1015,1016],{"class":225}," (\n",[216,1018,1019],{"class":87,"line":229},[216,1020,1021],{"class":225},"    QgsVectorLayer, QgsProject, QgsPalLayerSettings, QgsProperty,\n",[216,1023,1024],{"class":87,"line":236},[216,1025,1026],{"class":225},"    QgsLabeling, QgsVectorLayerSimpleLabeling,\n",[216,1028,1029],{"class":87,"line":249},[216,1030,1031],{"class":225},")\n",[216,1033,1034],{"class":87,"line":264},[216,1035,233],{"emptyLinePlaceholder":232},[216,1037,1038,1041,1043,1046,1048,1050,1053,1055,1058],{"class":87,"line":278},[216,1039,1040],{"class":225},"contours ",[216,1042,449],{"class":221},[216,1044,1045],{"class":225}," QgsVectorLayer(",[216,1047,375],{"class":242},[216,1049,516],{"class":225},[216,1051,1052],{"class":242},"\"contours\"",[216,1054,516],{"class":225},[216,1056,1057],{"class":242},"\"ogr\"",[216,1059,1031],{"class":225},[216,1061,1062],{"class":87,"line":291},[216,1063,1064],{"class":225},"QgsProject.instance().addMapLayer(contours)\n",[216,1066,1067],{"class":87,"line":304},[216,1068,233],{"emptyLinePlaceholder":232},[216,1070,1071,1074,1076],{"class":87,"line":317},[216,1072,1073],{"class":225},"settings ",[216,1075,449],{"class":221},[216,1077,1078],{"class":225}," QgsPalLayerSettings()\n",[216,1080,1081,1084,1086],{"class":87,"line":330},[216,1082,1083],{"class":225},"settings.fieldName ",[216,1085,449],{"class":221},[216,1087,1088],{"class":242}," \"elev\"\n",[216,1090,1091,1094,1096],{"class":87,"line":342},[216,1092,1093],{"class":225},"settings.placement ",[216,1095,449],{"class":221},[216,1097,1098],{"class":225}," QgsPalLayerSettings.Line\n",[216,1100,1101],{"class":87,"line":355},[216,1102,1103],{"class":225},"settings.lineSettings().setPlacementFlags(\n",[216,1105,1106,1109,1112],{"class":87,"line":367},[216,1107,1108],{"class":225},"    QgsLabeling.OnLine ",[216,1110,1111],{"class":221},"|",[216,1113,1114],{"class":225}," QgsLabeling.MapOrientation\n",[216,1116,1117],{"class":87,"line":380},[216,1118,1031],{"class":225},[216,1120,1121,1124,1126],{"class":87,"line":700},[216,1122,1123],{"class":225},"settings.isExpression ",[216,1125,449],{"class":221},[216,1127,1128],{"class":272}," False\n",[216,1130,1131,1134,1136],{"class":87,"line":726},[216,1132,1133],{"class":225},"settings.obstacle ",[216,1135,449],{"class":221},[216,1137,1128],{"class":272},[216,1139,1140],{"class":87,"line":746},[216,1141,233],{"emptyLinePlaceholder":232},[216,1143,1145,1148,1150],{"class":87,"line":1144},18,[216,1146,1147],{"class":225},"filter_settings ",[216,1149,449],{"class":221},[216,1151,1152],{"class":225}," settings\n",[216,1154,1156],{"class":87,"line":1155},19,[216,1157,1158],{"class":225},"filter_settings.dataDefinedProperties().setProperty(\n",[216,1160,1162],{"class":87,"line":1161},20,[216,1163,1164],{"class":225},"    QgsPalLayerSettings.Show,\n",[216,1166,1168,1171,1174],{"class":87,"line":1167},21,[216,1169,1170],{"class":225},"    QgsProperty.fromExpression(",[216,1172,1173],{"class":242},"'\"elev\" % 50 = 0'",[216,1175,1176],{"class":225},"),\n",[216,1178,1180],{"class":87,"line":1179},22,[216,1181,1031],{"class":225},[216,1183,1185],{"class":87,"line":1184},23,[216,1186,233],{"emptyLinePlaceholder":232},[216,1188,1190],{"class":87,"line":1189},24,[216,1191,1192],{"class":225},"contours.setLabeling(QgsVectorLayerSimpleLabeling(filter_settings))\n",[216,1194,1196,1199,1201],{"class":87,"line":1195},25,[216,1197,1198],{"class":225},"contours.setLabelsEnabled(",[216,1200,407],{"class":272},[216,1202,1031],{"class":225},[216,1204,1206],{"class":87,"line":1205},26,[216,1207,1208],{"class":225},"contours.triggerRepaint()\n",[14,1210,1211,1213,1214,1217,1218,1221,1222,1225,1226,1229,1230,1232,1233,1237],{},[185,1212,388],{}," The ",[202,1215,1216],{},"Show"," data-defined property is the clean way to label only some features — it evaluates per feature and suppresses the rest, without needing a separate layer or a subset string that would also hide the lines. ",[202,1219,1220],{},"placement = Line"," with ",[202,1223,1224],{},"OnLine"," puts the number in a gap in the contour rather than beside it, which is the cartographic convention and depends on the renderer being told to break the line under the label. ",[202,1227,1228],{},"obstacle = False"," stops the contours themselves being treated as things labels must avoid; leave it ",[202,1231,407],{}," and most labels are dropped. More on this in ",[21,1234,1236],{"href":1235},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcurved-labels-along-lines-pyqgis\u002F","curved labels along lines",".",[174,1239,1241],{"id":1240},"filled-bands-instead-of-lines","Filled bands instead of lines",[14,1243,1244],{},"Sometimes the deliverable is coloured elevation zones rather than lines.",[207,1246,1248],{"className":209,"code":1247,"language":211,"meta":212,"style":212},"processing.run(\"gdal:contour_polygon\", {\n    \"INPUT\": \"\u002Fdata\u002Fdem_27700.tif\",\n    \"BAND\": 1,\n    \"INTERVAL\": 50.0,\n    \"FIELD_NAME_MIN\": \"elev_min\",\n    \"FIELD_NAME_MAX\": \"elev_max\",\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fbands_50m.gpkg\",\n})\n",[202,1249,1250,1259,1269,1279,1290,1302,1314,1325],{"__ignoreMap":212},[216,1251,1252,1254,1257],{"class":87,"line":218},[216,1253,239],{"class":225},[216,1255,1256],{"class":242},"\"gdal:contour_polygon\"",[216,1258,246],{"class":225},[216,1260,1261,1263,1265,1267],{"class":87,"line":229},[216,1262,252],{"class":242},[216,1264,255],{"class":225},[216,1266,258],{"class":242},[216,1268,261],{"class":225},[216,1270,1271,1273,1275,1277],{"class":87,"line":236},[216,1272,267],{"class":242},[216,1274,255],{"class":225},[216,1276,273],{"class":272},[216,1278,261],{"class":225},[216,1280,1281,1283,1285,1288],{"class":87,"line":249},[216,1282,281],{"class":242},[216,1284,255],{"class":225},[216,1286,1287],{"class":272},"50.0",[216,1289,261],{"class":225},[216,1291,1292,1295,1297,1300],{"class":87,"line":264},[216,1293,1294],{"class":242},"    \"FIELD_NAME_MIN\"",[216,1296,255],{"class":225},[216,1298,1299],{"class":242},"\"elev_min\"",[216,1301,261],{"class":225},[216,1303,1304,1307,1309,1312],{"class":87,"line":278},[216,1305,1306],{"class":242},"    \"FIELD_NAME_MAX\"",[216,1308,255],{"class":225},[216,1310,1311],{"class":242},"\"elev_max\"",[216,1313,261],{"class":225},[216,1315,1316,1318,1320,1323],{"class":87,"line":291},[216,1317,370],{"class":242},[216,1319,255],{"class":225},[216,1321,1322],{"class":242},"\"\u002Fdata\u002Foutput\u002Fbands_50m.gpkg\"",[216,1324,261],{"class":225},[216,1326,1327],{"class":87,"line":304},[216,1328,383],{"class":225},[14,1330,1331,1333,1334,1338,1339,1342,1343,1347],{},[185,1332,388],{}," The polygon variant writes both bounds of each band into attributes, so a ",[21,1335,1337],{"href":1336},"\u002Fpyqgis-cartography-visualization\u002Fgraduated-categorized-renderers\u002F","graduated renderer"," on ",[202,1340,1341],{},"elev_min"," produces a hypsometric tint with no manual classification. Bands are considerably heavier than lines — a 50 m interval over a mountainous DEM easily produces tens of thousands of polygons — so it is worth ",[21,1344,1346],{"href":1345},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fsimplify-geometry-pyqgis\u002F","simplifying the geometries"," before publishing to a web service.",[174,1349,1351],{"id":1350},"qgis-version-compatibility","QGIS version compatibility",[14,1353,1354,1355,1357],{},"The examples target ",[185,1356,187],{}," (Python 3.12).",[1359,1360,1361,1377],"table",{},[1362,1363,1364],"thead",{},[1365,1366,1367,1371,1374],"tr",{},[1368,1369,1370],"th",{},"QGIS version",[1368,1372,1373],{},"Python",[1368,1375,1376],{},"Notes",[1378,1379,1380,1398,1412,1424,1435],"tbody",{},[1365,1381,1382,1386,1389],{},[1383,1384,1385],"td",{},"3.16 LTR",[1383,1387,1388],{},"3.7",[1383,1390,1391,1393,1394,1397],{},[202,1392,204],{}," present; ",[202,1395,1396],{},"gdal:contour_polygon"," requires GDAL 2.4+.",[1365,1399,1400,1403,1406],{},[1383,1401,1402],{},"3.22 LTR",[1383,1404,1405],{},"3.9",[1383,1407,1408,1409,1411],{},"Label ",[202,1410,1216],{}," data-defined property stable for per-feature filtering.",[1365,1413,1414,1417,1419],{},[1383,1415,1416],{},"3.28 LTR",[1383,1418,1405],{},[1383,1420,1421,1423],{},[202,1422,765],{}," available if decorative line smoothing is wanted.",[1365,1425,1426,1429,1432],{},[1383,1427,1428],{},"3.34 LTR",[1383,1430,1431],{},"3.12",[1383,1433,1434],{},"Baseline for this page.",[1365,1436,1437,1440,1442],{},[1383,1438,1439],{},"3.40+",[1383,1441,1431],{},[1383,1443,1444],{},"Contour output honours the input's coordinate precision more closely.",[174,1446,1448],{"id":1447},"troubleshooting","Troubleshooting",[179,1450,1451,1461,1469,1475,1484,1490],{},[182,1452,1453,1456,1457,1460],{},[185,1454,1455],{},"Contour rings around every gap."," Nodata is undeclared, so ",[202,1458,1459],{},"-9999"," is being contoured as terrain.",[182,1462,1463,389,1466,1468],{},[185,1464,1465],{},"The output has no elevation attribute.",[202,1467,392],{}," was omitted. Re-run; there is no way to recover it afterwards.",[182,1470,1471,1474],{},[185,1472,1473],{},"Lines are blocky and follow the pixel grid."," The DEM is integer-valued or resampled. Filter the raster, not the lines.",[182,1476,1477,389,1480,1483],{},[185,1478,1479],{},"No labels appear.",[202,1481,1482],{},"setLabelsEnabled(True)"," was not called, or the contours are acting as obstacles to their own labels.",[182,1485,1486,1489],{},[185,1487,1488],{},"The map is a mat of lines."," The interval is too fine for the output scale. Compute the floor from the steepest ground and round up.",[182,1491,1492,1495],{},[185,1493,1494],{},"Smoothed lines disagree with the DEM."," They were smoothed after contouring. Redo the smoothing on the raster.",[174,1497,1499],{"id":1498},"conclusion","Conclusion",[14,1501,1502,1503,1505,1506,1508,1509,1511],{},"Set ",[202,1504,392],{},", leave ",[202,1507,400],{}," off, and pick the interval from the steepest ground and the output scale rather than from habit. Fix terracing in the raster before contouring, use a per-feature ",[202,1510,1216],{}," expression for index contours, and reach for the polygon variant only when the deliverable is genuinely a tinted band map.",[174,1513,1515],{"id":1514},"frequently-asked-questions","Frequently Asked Questions",[14,1517,1518,1521,1522,1524,1525,1528],{},[185,1519,1520],{},"Can I generate contours at specific non-uniform levels?","\nYes — ",[202,1523,204],{}," accepts a ",[202,1526,1527],{},"FIXED_LEVELS"," string of comma-separated values instead of an interval, which is how flood-level or bathymetric contours at conventional depths are produced.",[14,1530,1531,1534],{},[185,1532,1533],{},"Why are my contours in the wrong place after reprojecting?","\nContours are derived at the DEM's grid, so reprojecting the lines afterwards resamples geometry rather than re-deriving it. Reproject the DEM and contour the reprojected raster instead.",[14,1536,1537,1540,1541,1545],{},[185,1538,1539],{},"How do I get closed contours for a bounded area?","\nContours are open where they meet the raster edge. Clip the DEM to a slightly larger extent than the study area, contour, then ",[21,1542,1544],{"href":1543},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fclip-vector-layer-pyqgis\u002F","clip the lines"," back — that way each line is cut cleanly rather than ending at an arbitrary tile boundary.",[14,1547,1548,1551],{},[185,1549,1550],{},"Is there a native contour algorithm?","\nThe GDAL one is the standard route and is what the interface uses. There is no separately maintained native equivalent, and there is no need for one.",[174,1553,1555],{"id":1554},"related","Related",[179,1557,1558,1563,1569,1574,1579],{},[182,1559,1560,1562],{},[21,1561,24],{"href":23}," — the guide this recipe belongs to",[182,1564,1565],{},[21,1566,1568],{"href":1567},"\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fgenerate-slope-aspect-hillshade-pyqgis\u002F","Generate Slope, Aspect and Hillshade in PyQGIS",[182,1570,1571],{},[21,1572,1573],{"href":417},"Build a TIN Interpolation in PyQGIS",[182,1575,1576],{},[21,1577,1578],{"href":1235},"Curved Labels Along Lines in PyQGIS",[182,1580,1581],{},[21,1582,1583],{"href":1345},"Simplify a Geometry in PyQGIS",[1585,1586,1587],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":212,"searchDepth":229,"depth":229,"links":1589},[1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601],{"id":176,"depth":229,"text":177},{"id":197,"depth":229,"text":198},{"id":422,"depth":229,"text":423},{"id":539,"depth":229,"text":540},{"id":834,"depth":229,"text":835},{"id":994,"depth":229,"text":995},{"id":1240,"depth":229,"text":1241},{"id":1350,"depth":229,"text":1351},{"id":1447,"depth":229,"text":1448},{"id":1498,"depth":229,"text":1499},{"id":1514,"depth":229,"text":1515},{"id":1554,"depth":229,"text":1555},"Generate contour lines and filled bands from an elevation raster with gdal:contour — choosing an interval, index contours, smoothing terracing artefacts, and labelling along the line.","md",{"slug":1605,"type":1606,"breadcrumb":1607,"datePublished":1608,"dateModified":1608},"create-contours-from-dem-pyqgis","article","Contours from a DEM","2026-08-27","\u002Fspatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fcreate-contours-from-dem-pyqgis",{"title":5,"description":1602},"spatial-data-processing-automation\u002Fterrain-and-interpolation-analysis\u002Fcreate-contours-from-dem-pyqgis\u002Findex","LFjWRO53YatGu7YUR_NJBY6fsrUvFBUQ6n5b1YE_YoA",1787823363905]