[{"data":1,"prerenderedAt":2015},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows":3},{"id":4,"title":5,"body":6,"description":2003,"extension":2004,"meta":2005,"navigation":359,"path":2011,"seo":2012,"stem":2013,"__hash__":2014},"docs\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Findex.md","Raster Analysis Workflows in PyQGIS",{"type":7,"value":8,"toc":1990},"minimark",[9,14,24,47,201,206,217,228,232,267,271,278,965,970,1018,1142,1146,1230,1234,1251,1255,1270,1274,1281,1379,1386,1397,1401,1410,1425,1631,1653,1656,1681,1693,1697,1744,1748,1766,1788,1805,1817,1833,1843,1854,1870,1876,1882,1891,1897,1907,1920,1926,1932,1936,1986],[10,11,13],"h1",{"id":12},"raster-analysis-workflows-in-qgis-and-pyqgis","Raster Analysis Workflows in QGIS and PyQGIS",[15,16,17,18,23],"p",{},"Raster analysis forms the backbone of geospatial modeling, enabling practitioners to extract meaningful patterns from continuous surface data. Whether calculating terrain derivatives, normalizing multispectral imagery, or deriving land cover classifications, structured raster analysis workflows ensure reproducibility, scalability, and auditability. Within the broader framework of ",[19,20,22],"a",{"href":21},"\u002Fspatial-data-processing-automation\u002F","Spatial Data Processing & Automation",", PyQGIS provides a programmatic bridge between QGIS's graphical interface and Python's analytical ecosystem.",[15,25,26,27,31,32,36,37,41,42,46],{},"This guide sits alongside the section's other automation topics — from ",[19,28,30],{"href":29},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002F","vector data manipulation"," to ",[19,33,35],{"href":34},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","coordinate reference systems"," — and focuses squarely on the raster side. It outlines a production-ready workflow, complete with prerequisites, an executable code pattern, and troubleshooting strategies tailored for both interactive console execution and headless automation. Two focused how-tos drill deeper into the operations touched on here: ",[19,38,40],{"href":39},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fclip-raster-by-mask-layer-pyqgis\u002F","clipping a raster by a mask layer"," and ",[19,43,45],{"href":44},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fcalculate-raster-statistics-pyqgis\u002F","calculating raster statistics",".",[15,48,49],{},[50,51,56,60,64,71,88,98,107,112,117,121,125,128,131,135,139,142,145,149,153,157,161,163,166,169,172,197],"svg",{"viewBox":52,"role":53,"ariaLabel":54,"xmlns":55},"0 0 800 380","img","Raster analysis workflow from loading a raster through band selection, computation, masking and export","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[57,58,59],"title",{},"PyQGIS raster analysis workflow",[61,62,63],"desc",{},"A multi-band raster is loaded and validated, a band is selected, then algebra, statistics or reclassification is computed, the result is clipped to a vector mask, and the output is exported with metadata. Each step is chained through processing.run with intermediate outputs written to disk.",[65,66],"rect",{"x":67,"y":67,"width":68,"height":69,"fill":70},"0","800","380","#f6f3ea",[72,73,74],"defs",{},[75,76,83],"marker",{"id":77,"viewBox":78,"refX":79,"refY":80,"markerWidth":81,"markerHeight":81,"orient":82},"rw-arrow","0 0 10 10","8","5","7","auto-start-reverse",[84,85],"path",{"d":86,"fill":87},"M0 0 L10 5 L0 10 z","#2f3b35",[65,89],{"x":90,"y":91,"width":92,"height":93,"rx":94,"fill":95,"stroke":96,"style":97},"30","40","180","92","10","#fffdf7","#0d9488","stroke-width:2.5",[99,100,106],"text",{"x":101,"y":102,"style":103,"fill":104,"textAnchor":105},"120","72","text-anchor:middle;font-size:15px;font-weight:bold;font-family:sans-serif","#17211d","middle","1 · Load & Validate",[99,108,111],{"x":101,"y":109,"style":110,"fill":87,"textAnchor":105},"95","text-anchor:middle;font-size:12px;font-family:monospace;opacity:0.72","QgsRasterLayer",[99,113,116],{"x":101,"y":114,"style":115,"fill":87,"textAnchor":105},"114","text-anchor:middle;font-size:12px;font-family:sans-serif;opacity:0.72","bands · CRS · extent",[65,118],{"x":119,"y":91,"width":92,"height":93,"rx":94,"fill":95,"stroke":120,"style":97},"310","#3b82f6",[99,122,124],{"x":123,"y":102,"style":103,"fill":104,"textAnchor":105},"400","2 · Band Selection",[99,126,127],{"x":123,"y":109,"style":110,"fill":87,"textAnchor":105},"@1 · @2 · nodata",[99,129,130],{"x":123,"y":114,"style":115,"fill":87,"textAnchor":105},"renderer · dtype",[65,132],{"x":133,"y":91,"width":92,"height":93,"rx":94,"fill":95,"stroke":134,"style":97},"590","#16a34a",[99,136,138],{"x":137,"y":102,"style":103,"fill":104,"textAnchor":105},"680","3 · Compute",[99,140,141],{"x":137,"y":109,"style":115,"fill":87,"textAnchor":105},"algebra · statistics",[99,143,144],{"x":137,"y":114,"style":115,"fill":87,"textAnchor":105},"reclassify",[65,146],{"x":119,"y":147,"width":92,"height":93,"rx":94,"fill":95,"stroke":148,"style":97},"228","#d97706",[99,150,152],{"x":123,"y":151,"style":103,"fill":104,"textAnchor":105},"260","4 · Mask \u002F Clip",[99,154,156],{"x":123,"y":155,"style":115,"fill":87,"textAnchor":105},"283","vector boundary",[99,158,160],{"x":123,"y":159,"style":115,"fill":87,"textAnchor":105},"302","crop to cutline",[65,162],{"x":90,"y":147,"width":92,"height":93,"rx":94,"fill":95,"stroke":96,"style":97},[99,164,165],{"x":101,"y":151,"style":103,"fill":104,"textAnchor":105},"5 · Export",[99,167,168],{"x":101,"y":155,"style":115,"fill":87,"textAnchor":105},"GeoTIFF + stats",[99,170,171],{"x":101,"y":159,"style":115,"fill":87,"textAnchor":105},"validate ranges",[173,174,176,183,187,191,194],"g",{"stroke":175,"style":97},"currentColor",[177,178],"line",{"x1":179,"y1":180,"x2":181,"y2":180,"style":182},"210","86","304","marker-end:url(#rw-arrow)",[177,184],{"x1":185,"y1":180,"x2":186,"y2":180,"style":182},"490","584",[177,188],{"x1":137,"y1":189,"x2":137,"y2":190},"132","274",[177,192],{"x1":137,"y1":190,"x2":193,"y2":190,"style":182},"496",[177,195],{"x1":119,"y1":190,"x2":196,"y2":190,"style":182},"216",[99,198,200],{"x":123,"y":199,"style":115,"fill":87,"textAnchor":105},"358","processing.run() chains each step — intermediate outputs written to disk for memory safety",[202,203,205],"h2",{"id":204},"prerequisites","Prerequisites",[15,207,208,209,41,213,216],{},"Before executing raster analysis, ensure your environment meets foundational technical requirements. Install QGIS 3.28 or newer, which ships with a bundled Python interpreter and the PyQGIS API. Verify that the ",[210,211,212],"code",{},"processing",[210,214,215],{},"qgis"," modules are accessible via the Python console or standalone script. Prepare your input datasets: multi-band rasters (GeoTIFF, NetCDF, or Cloud Optimized GeoTIFF) and optional vector masks for spatial subsetting.",[15,218,219,220,223,224,227],{},"Crucially, align all datasets to a common projection. Misaligned ",[19,221,222],{"href":34},"Coordinate Reference Systems"," will silently corrupt spatial operations, producing shifted outputs, failed calculations, or distorted statistical summaries. Use ",[210,225,226],{},"gdalinfo"," or QGIS's layer properties to confirm EPSG codes, pixel spacing, and extent boundaries. Additionally, ensure sufficient disk space for intermediate outputs and verify that GDAL drivers for your target formats are enabled in your QGIS configuration.",[202,229,231],{"id":230},"step-by-step-workflow","Step-by-Step Workflow",[233,234,235,243,249,255,261],"ol",{},[236,237,238,242],"li",{},[239,240,241],"strong",{},"Initialize the PyQGIS Environment:"," Load required modules, configure the QGIS application instance (if running standalone), and register the native processing algorithms.",[236,244,245,248],{},[239,246,247],{},"Load and Validate Raster Data:"," Programmatically add rasters to the project, verify band counts, check for missing data flags, and confirm spatial extents.",[236,250,251,254],{},[239,252,253],{},"Preprocess and Align:"," Resample rasters to a uniform resolution, align extents, and apply vector masks where necessary to reduce computational overhead.",[236,256,257,260],{},[239,258,259],{},"Execute Analysis:"," Run raster calculator expressions, derive descriptive statistics, or apply classification algorithms using QGIS processing tools.",[236,262,263,266],{},[239,264,265],{},"Export and Document:"," Save outputs with proper metadata, validate results against expected ranges, and clean up temporary layers to free memory.",[202,268,270],{"id":269},"code-breakdown-tested-pattern","Code Breakdown & Tested Pattern",[15,272,273,274,277],{},"The following script demonstrates a complete, standalone PyQGIS workflow. It loads a raster, ",[19,275,276],{"href":39},"clips it using a vector boundary",", computes a scaled band value, and exports the result. This pattern leverages QGIS's Processing framework for memory-safe execution and can be adapted to headless servers or integrated into larger CI\u002FCD pipelines.",[279,280,285],"pre",{"className":281,"code":282,"language":283,"meta":284,"style":284},"language-python shiki shiki-themes github-dark","import os\nimport logging\nfrom qgis.core import (\n    QgsApplication, QgsRasterLayer, QgsVectorLayer, QgsProcessingFeedback,\n)\nfrom qgis.analysis import QgsNativeAlgorithms\nimport processing\n\n# Configure logging for traceability\nlogging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')\n\n# 1. Initialize QGIS (standalone mode)\nqgs = QgsApplication([], False)\nqgs.initQgis()\nQgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())\n\n# 2. Define paths\nraster_path = \"\u002Fdata\u002Finput_multispectral.tif\"\nvector_path = \"\u002Fdata\u002Fstudy_area_boundary.gpkg\"\noutput_dir = \"\u002Foutput\u002Fraster_analysis\"\nos.makedirs(output_dir, exist_ok=True)\n\n# 3. Load and validate layers\nraster = QgsRasterLayer(raster_path, \"Input Raster\")\nvector = QgsVectorLayer(vector_path, \"Boundary\", \"ogr\")\nif not raster.isValid() or not vector.isValid():\n    qgs.exitQgis()\n    raise RuntimeError(\"Invalid input layers. Check paths, formats, and GDAL drivers.\")\n\nlogging.info(f\"Loaded raster: {raster.width()}x{raster.height()} pixels | CRS: {raster.crs().authid()}\")\n\n# 4. Clip raster to vector extent using Processing (memory-safe)\nclip_output = os.path.join(output_dir, \"clipped_raster.tif\")\nprocessing.run(\"gdal:cliprasterbymasklayer\", {\n    'INPUT': raster_path,\n    'MASK': vector_path,\n    'SOURCE_CRS': raster.crs(),\n    'TARGET_CRS': raster.crs(),\n    'OUTPUT': clip_output,\n    'NODATA': -9999,\n    'ALPHA_BAND': False,\n    'CROP_TO_CUTLINE': True,\n    'KEEP_RESOLUTION': True,\n    'DATA_TYPE': 0,\n    'MULTITHREADING': True,\n})\nlogging.info(\"Clip operation completed.\")\n\n# 5. Raster calculation via Processing\ncalc_output = os.path.join(output_dir, \"scaled_output.tif\")\nprocessing.run(\"native:rastercalculator\", {\n    'EXPRESSION': '\"clipped_raster@1\" \u002F 10000.0 * 100',\n    'LAYERS': [clip_output],\n    'CELLSIZE': 0,\n    'EXTENT': None,\n    'CRS': None,\n    'OUTPUT': calc_output,\n})\nlogging.info(\"Raster calculation completed successfully.\")\n\n# 6. Cleanup\nqgs.exitQgis()\n","python","",[210,286,287,299,307,321,327,333,346,354,361,368,413,418,424,440,446,452,457,463,474,485,496,512,517,523,539,560,580,586,603,608,654,659,665,681,693,702,711,720,728,737,754,766,778,790,802,814,820,830,835,841,856,866,879,888,900,913,925,933,938,948,953,959],{"__ignoreMap":284},[288,289,291,295],"span",{"class":177,"line":290},1,[288,292,294],{"class":293},"snl16","import",[288,296,298],{"class":297},"s95oV"," os\n",[288,300,302,304],{"class":177,"line":301},2,[288,303,294],{"class":293},[288,305,306],{"class":297}," logging\n",[288,308,310,313,316,318],{"class":177,"line":309},3,[288,311,312],{"class":293},"from",[288,314,315],{"class":297}," qgis.core ",[288,317,294],{"class":293},[288,319,320],{"class":297}," (\n",[288,322,324],{"class":177,"line":323},4,[288,325,326],{"class":297},"    QgsApplication, QgsRasterLayer, QgsVectorLayer, QgsProcessingFeedback,\n",[288,328,330],{"class":177,"line":329},5,[288,331,332],{"class":297},")\n",[288,334,336,338,341,343],{"class":177,"line":335},6,[288,337,312],{"class":293},[288,339,340],{"class":297}," qgis.analysis ",[288,342,294],{"class":293},[288,344,345],{"class":297}," QgsNativeAlgorithms\n",[288,347,349,351],{"class":177,"line":348},7,[288,350,294],{"class":293},[288,352,353],{"class":297}," processing\n",[288,355,357],{"class":177,"line":356},8,[288,358,360],{"emptyLinePlaceholder":359},true,"\n",[288,362,364],{"class":177,"line":363},9,[288,365,367],{"class":366},"sjoCn","# Configure logging for traceability\n",[288,369,371,374,378,381,384,388,391,394,396,400,403,406,409,411],{"class":177,"line":370},10,[288,372,373],{"class":297},"logging.basicConfig(",[288,375,377],{"class":376},"s9osk","level",[288,379,380],{"class":293},"=",[288,382,383],{"class":297},"logging.",[288,385,387],{"class":386},"sDLfK","INFO",[288,389,390],{"class":297},", ",[288,392,393],{"class":376},"format",[288,395,380],{"class":293},[288,397,399],{"class":398},"sU2Wk","'",[288,401,402],{"class":386},"%(levelname)s",[288,404,405],{"class":398},": ",[288,407,408],{"class":386},"%(message)s",[288,410,399],{"class":398},[288,412,332],{"class":297},[288,414,416],{"class":177,"line":415},11,[288,417,360],{"emptyLinePlaceholder":359},[288,419,421],{"class":177,"line":420},12,[288,422,423],{"class":366},"# 1. Initialize QGIS (standalone mode)\n",[288,425,427,430,432,435,438],{"class":177,"line":426},13,[288,428,429],{"class":297},"qgs ",[288,431,380],{"class":293},[288,433,434],{"class":297}," QgsApplication([], ",[288,436,437],{"class":386},"False",[288,439,332],{"class":297},[288,441,443],{"class":177,"line":442},14,[288,444,445],{"class":297},"qgs.initQgis()\n",[288,447,449],{"class":177,"line":448},15,[288,450,451],{"class":297},"QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())\n",[288,453,455],{"class":177,"line":454},16,[288,456,360],{"emptyLinePlaceholder":359},[288,458,460],{"class":177,"line":459},17,[288,461,462],{"class":366},"# 2. Define paths\n",[288,464,466,469,471],{"class":177,"line":465},18,[288,467,468],{"class":297},"raster_path ",[288,470,380],{"class":293},[288,472,473],{"class":398}," \"\u002Fdata\u002Finput_multispectral.tif\"\n",[288,475,477,480,482],{"class":177,"line":476},19,[288,478,479],{"class":297},"vector_path ",[288,481,380],{"class":293},[288,483,484],{"class":398}," \"\u002Fdata\u002Fstudy_area_boundary.gpkg\"\n",[288,486,488,491,493],{"class":177,"line":487},20,[288,489,490],{"class":297},"output_dir ",[288,492,380],{"class":293},[288,494,495],{"class":398}," \"\u002Foutput\u002Fraster_analysis\"\n",[288,497,499,502,505,507,510],{"class":177,"line":498},21,[288,500,501],{"class":297},"os.makedirs(output_dir, ",[288,503,504],{"class":376},"exist_ok",[288,506,380],{"class":293},[288,508,509],{"class":386},"True",[288,511,332],{"class":297},[288,513,515],{"class":177,"line":514},22,[288,516,360],{"emptyLinePlaceholder":359},[288,518,520],{"class":177,"line":519},23,[288,521,522],{"class":366},"# 3. Load and validate layers\n",[288,524,526,529,531,534,537],{"class":177,"line":525},24,[288,527,528],{"class":297},"raster ",[288,530,380],{"class":293},[288,532,533],{"class":297}," QgsRasterLayer(raster_path, ",[288,535,536],{"class":398},"\"Input Raster\"",[288,538,332],{"class":297},[288,540,542,545,547,550,553,555,558],{"class":177,"line":541},25,[288,543,544],{"class":297},"vector ",[288,546,380],{"class":293},[288,548,549],{"class":297}," QgsVectorLayer(vector_path, ",[288,551,552],{"class":398},"\"Boundary\"",[288,554,390],{"class":297},[288,556,557],{"class":398},"\"ogr\"",[288,559,332],{"class":297},[288,561,563,566,569,572,575,577],{"class":177,"line":562},26,[288,564,565],{"class":293},"if",[288,567,568],{"class":293}," not",[288,570,571],{"class":297}," raster.isValid() ",[288,573,574],{"class":293},"or",[288,576,568],{"class":293},[288,578,579],{"class":297}," vector.isValid():\n",[288,581,583],{"class":177,"line":582},27,[288,584,585],{"class":297},"    qgs.exitQgis()\n",[288,587,589,592,595,598,601],{"class":177,"line":588},28,[288,590,591],{"class":293},"    raise",[288,593,594],{"class":386}," RuntimeError",[288,596,597],{"class":297},"(",[288,599,600],{"class":398},"\"Invalid input layers. Check paths, formats, and GDAL drivers.\"",[288,602,332],{"class":297},[288,604,606],{"class":177,"line":605},29,[288,607,360],{"emptyLinePlaceholder":359},[288,609,611,614,617,620,623,626,629,632,634,637,639,642,644,647,649,652],{"class":177,"line":610},30,[288,612,613],{"class":297},"logging.info(",[288,615,616],{"class":293},"f",[288,618,619],{"class":398},"\"Loaded raster: ",[288,621,622],{"class":386},"{",[288,624,625],{"class":297},"raster.width()",[288,627,628],{"class":386},"}",[288,630,631],{"class":398},"x",[288,633,622],{"class":386},[288,635,636],{"class":297},"raster.height()",[288,638,628],{"class":386},[288,640,641],{"class":398}," pixels | CRS: ",[288,643,622],{"class":386},[288,645,646],{"class":297},"raster.crs().authid()",[288,648,628],{"class":386},[288,650,651],{"class":398},"\"",[288,653,332],{"class":297},[288,655,657],{"class":177,"line":656},31,[288,658,360],{"emptyLinePlaceholder":359},[288,660,662],{"class":177,"line":661},32,[288,663,664],{"class":366},"# 4. Clip raster to vector extent using Processing (memory-safe)\n",[288,666,668,671,673,676,679],{"class":177,"line":667},33,[288,669,670],{"class":297},"clip_output ",[288,672,380],{"class":293},[288,674,675],{"class":297}," os.path.join(output_dir, ",[288,677,678],{"class":398},"\"clipped_raster.tif\"",[288,680,332],{"class":297},[288,682,684,687,690],{"class":177,"line":683},34,[288,685,686],{"class":297},"processing.run(",[288,688,689],{"class":398},"\"gdal:cliprasterbymasklayer\"",[288,691,692],{"class":297},", {\n",[288,694,696,699],{"class":177,"line":695},35,[288,697,698],{"class":398},"    'INPUT'",[288,700,701],{"class":297},": raster_path,\n",[288,703,705,708],{"class":177,"line":704},36,[288,706,707],{"class":398},"    'MASK'",[288,709,710],{"class":297},": vector_path,\n",[288,712,714,717],{"class":177,"line":713},37,[288,715,716],{"class":398},"    'SOURCE_CRS'",[288,718,719],{"class":297},": raster.crs(),\n",[288,721,723,726],{"class":177,"line":722},38,[288,724,725],{"class":398},"    'TARGET_CRS'",[288,727,719],{"class":297},[288,729,731,734],{"class":177,"line":730},39,[288,732,733],{"class":398},"    'OUTPUT'",[288,735,736],{"class":297},": clip_output,\n",[288,738,740,743,745,748,751],{"class":177,"line":739},40,[288,741,742],{"class":398},"    'NODATA'",[288,744,405],{"class":297},[288,746,747],{"class":293},"-",[288,749,750],{"class":386},"9999",[288,752,753],{"class":297},",\n",[288,755,757,760,762,764],{"class":177,"line":756},41,[288,758,759],{"class":398},"    'ALPHA_BAND'",[288,761,405],{"class":297},[288,763,437],{"class":386},[288,765,753],{"class":297},[288,767,769,772,774,776],{"class":177,"line":768},42,[288,770,771],{"class":398},"    'CROP_TO_CUTLINE'",[288,773,405],{"class":297},[288,775,509],{"class":386},[288,777,753],{"class":297},[288,779,781,784,786,788],{"class":177,"line":780},43,[288,782,783],{"class":398},"    'KEEP_RESOLUTION'",[288,785,405],{"class":297},[288,787,509],{"class":386},[288,789,753],{"class":297},[288,791,793,796,798,800],{"class":177,"line":792},44,[288,794,795],{"class":398},"    'DATA_TYPE'",[288,797,405],{"class":297},[288,799,67],{"class":386},[288,801,753],{"class":297},[288,803,805,808,810,812],{"class":177,"line":804},45,[288,806,807],{"class":398},"    'MULTITHREADING'",[288,809,405],{"class":297},[288,811,509],{"class":386},[288,813,753],{"class":297},[288,815,817],{"class":177,"line":816},46,[288,818,819],{"class":297},"})\n",[288,821,823,825,828],{"class":177,"line":822},47,[288,824,613],{"class":297},[288,826,827],{"class":398},"\"Clip operation completed.\"",[288,829,332],{"class":297},[288,831,833],{"class":177,"line":832},48,[288,834,360],{"emptyLinePlaceholder":359},[288,836,838],{"class":177,"line":837},49,[288,839,840],{"class":366},"# 5. Raster calculation via Processing\n",[288,842,844,847,849,851,854],{"class":177,"line":843},50,[288,845,846],{"class":297},"calc_output ",[288,848,380],{"class":293},[288,850,675],{"class":297},[288,852,853],{"class":398},"\"scaled_output.tif\"",[288,855,332],{"class":297},[288,857,859,861,864],{"class":177,"line":858},51,[288,860,686],{"class":297},[288,862,863],{"class":398},"\"native:rastercalculator\"",[288,865,692],{"class":297},[288,867,869,872,874,877],{"class":177,"line":868},52,[288,870,871],{"class":398},"    'EXPRESSION'",[288,873,405],{"class":297},[288,875,876],{"class":398},"'\"clipped_raster@1\" \u002F 10000.0 * 100'",[288,878,753],{"class":297},[288,880,882,885],{"class":177,"line":881},53,[288,883,884],{"class":398},"    'LAYERS'",[288,886,887],{"class":297},": [clip_output],\n",[288,889,891,894,896,898],{"class":177,"line":890},54,[288,892,893],{"class":398},"    'CELLSIZE'",[288,895,405],{"class":297},[288,897,67],{"class":386},[288,899,753],{"class":297},[288,901,903,906,908,911],{"class":177,"line":902},55,[288,904,905],{"class":398},"    'EXTENT'",[288,907,405],{"class":297},[288,909,910],{"class":386},"None",[288,912,753],{"class":297},[288,914,916,919,921,923],{"class":177,"line":915},56,[288,917,918],{"class":398},"    'CRS'",[288,920,405],{"class":297},[288,922,910],{"class":386},[288,924,753],{"class":297},[288,926,928,930],{"class":177,"line":927},57,[288,929,733],{"class":398},[288,931,932],{"class":297},": calc_output,\n",[288,934,936],{"class":177,"line":935},58,[288,937,819],{"class":297},[288,939,941,943,946],{"class":177,"line":940},59,[288,942,613],{"class":297},[288,944,945],{"class":398},"\"Raster calculation completed successfully.\"",[288,947,332],{"class":297},[288,949,951],{"class":177,"line":950},60,[288,952,360],{"emptyLinePlaceholder":359},[288,954,956],{"class":177,"line":955},61,[288,957,958],{"class":366},"# 6. Cleanup\n",[288,960,962],{"class":177,"line":961},62,[288,963,964],{"class":297},"qgs.exitQgis()\n",[15,966,967],{},[239,968,969],{},"Key Implementation Notes:",[971,972,973,984,994,1004,1007],"ul",{},[236,974,975,976,979,980,983],{},"The script uses ",[210,977,978],{},"processing.run()"," exclusively, which handles memory mapping, tiling, and edge cases more robustly than legacy ",[210,981,982],{},"QgsRasterCalculator"," bindings.",[236,985,986,987,41,990,993],{},"Always pass ",[210,988,989],{},"SOURCE_CRS",[210,991,992],{},"TARGET_CRS"," explicitly during clipping to prevent silent reprojection failures.",[236,995,996,997,390,1000,1003],{},"For multi-band operations, reference bands using ",[210,998,999],{},"@1",[210,1001,1002],{},"@2",", etc., inside the expression string. Processing automatically resolves layer paths when the layer name matches.",[236,1005,1006],{},"When chaining operations, write intermediate outputs to disk rather than holding them in memory, especially for datasets exceeding 2GB or when running on constrained hardware.",[236,1008,1009,1010,1013,1014,1017],{},"The raster calculator algorithm ID is ",[210,1011,1012],{},"native:rastercalculator"," in QGIS 3.34+ (replacing ",[210,1015,1016],{},"qgis:rastercalculator","). Verify the correct ID for your QGIS version via the Processing Toolbox.",[15,1019,1020],{},[50,1021,1024,1027,1030,1036,1043,1047,1052,1059,1064,1066,1070,1073,1077,1082,1087,1091,1095,1099,1103,1107,1111,1115,1119,1123,1135,1139],{"viewBox":1022,"role":53,"ariaLabel":1023,"xmlns":55},"14 54 772 275","How band references and nodata values flow through a raster calculator expression to produce an output raster",[57,1025,1026],{},"Band referencing and nodata propagation in a raster calculator expression",[61,1028,1029],{},"A multi-band raster exposes each band as a 1-based reference such as nir@1 and red@2. These feed a native rastercalculator expression that computes NDVI and guards nodata pixels with an if() test, producing a float32 output raster whose nodata value is set explicitly to -9999.",[65,1031],{"x":1032,"y":1033,"width":1034,"height":1035,"fill":70},"14","54","772","275",[72,1037,1038],{},[75,1039,1041],{"id":1040,"viewBox":78,"refX":79,"refY":80,"markerWidth":81,"markerHeight":81,"orient":82},"bc-arrow",[84,1042],{"d":86,"fill":87},[65,1044],{"x":90,"y":1045,"width":92,"height":1046,"rx":94,"fill":95,"stroke":96,"style":97},"70","170",[99,1048,1051],{"x":101,"y":1049,"style":1050,"fill":104,"textAnchor":105},"96","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","Multi-band raster",[65,1053],{"x":1054,"y":1055,"width":1056,"height":1057,"rx":1058,"fill":95},"45","110","150","32","6",[99,1060,1063],{"x":101,"y":1061,"style":1062,"fill":87,"textAnchor":105},"131","text-anchor:middle;font-size:13px;font-family:monospace","\"nir@1\"",[65,1065],{"x":1054,"y":1056,"width":1056,"height":1057,"rx":1058,"fill":95},[99,1067,1069],{"x":101,"y":1068,"style":1062,"fill":87,"textAnchor":105},"171","\"red@2\"",[65,1071],{"x":1054,"y":1072,"width":1056,"height":1057,"rx":1058,"fill":95},"190",[99,1074,1076],{"x":101,"y":1075,"style":1062,"fill":87,"textAnchor":105},"211","\"grn@3\"",[65,1078],{"x":1079,"y":1080,"width":1081,"height":1056,"rx":94,"fill":95,"stroke":134,"style":97},"300","90","252",[99,1083,1012],{"x":1084,"y":1085,"style":1086,"fill":104,"textAnchor":105},"426","116","text-anchor:middle;font-size:13px;font-weight:bold;font-family:monospace",[99,1088,1090],{"x":1084,"y":1089,"style":1062,"fill":87,"textAnchor":105},"146","( \"nir@1\" - \"red@2\" )",[99,1092,1094],{"x":1084,"y":1093,"style":1062,"fill":87,"textAnchor":105},"167","\u002F ( \"nir@1\" + \"red@2\" )",[99,1096,1098],{"x":1084,"y":1097,"style":115,"fill":87,"textAnchor":105},"197","nodata guard:",[99,1100,1102],{"x":1084,"y":1101,"style":1062,"fill":87,"textAnchor":105},"218","if(@1 \u003C 0, -9999, ndvi)",[65,1104],{"x":1105,"y":1080,"width":1106,"height":1056,"rx":94,"fill":95,"stroke":148,"style":97},"612","158",[99,1108,1110],{"x":1109,"y":101,"style":1050,"fill":104,"textAnchor":105},"691","NDVI output",[99,1112,1114],{"x":1109,"y":1113,"style":115,"fill":87,"textAnchor":105},"148","float32 raster",[99,1116,1118],{"x":1109,"y":1117,"style":115,"fill":87,"textAnchor":105},"174","range -1 … 1",[99,1120,1122],{"x":1109,"y":1121,"style":1062,"fill":87,"textAnchor":105},"200","nodata = -9999",[173,1124,1125,1130],{"stroke":175,"style":97},[177,1126],{"x1":179,"y1":1127,"x2":1128,"y2":1127,"style":1129},"155","294","marker-end:url(#bc-arrow)",[177,1131],{"x1":1132,"y1":1133,"x2":1134,"y2":1133,"style":1129},"552","165","606",[99,1136,1138],{"x":123,"y":1137,"style":115,"fill":87,"textAnchor":105},"290","Bands are 1-based — @1 is the first band, not @0.",[99,1140,1141],{"x":123,"y":119,"style":115,"fill":87,"textAnchor":105},"A nodata pixel in any input propagates to the output unless guarded with if().",[202,1143,1145],{"id":1144},"common-errors-fixes","Common Errors & Fixes",[971,1147,1148,1162,1176,1198,1211],{},[236,1149,1150,1153,1154,1157,1158,1161],{},[239,1151,1152],{},"CRS Mismatch During Clipping:"," If the output raster appears shifted or blank, verify that both input layers share the same EPSG code. PyQGIS does not automatically reproject during ",[210,1155,1156],{},"gdal:cliprasterbymasklayer",". Pre-align layers using ",[210,1159,1160],{},"gdal:warpreproject"," or QGIS's reprojection tools before execution.",[236,1163,1164,1167,1168,1171,1172,1175],{},[239,1165,1166],{},"Memory Allocation Failures:"," Large rasters trigger ",[210,1169,1170],{},"std::bad_alloc"," errors. Mitigate this by enabling ",[210,1173,1174],{},"MULTITHREADING: True"," in processing parameters, reducing block size, or using Virtual Raster (VRT) intermediates to avoid loading entire datasets into RAM.",[236,1177,1178,1181,1182,1185,1186,1189,1190,1193,1194,1197],{},[239,1179,1180],{},"Null Value Propagation:"," Raster calculations often return ",[210,1183,1184],{},"NaN"," or ",[210,1187,1188],{},"-9999"," where masks overlap imperfectly. Explicitly define ",[210,1191,1192],{},"NODATA"," in processing parameters and use conditional expressions like ",[210,1195,1196],{},"if(\"layer@1\" \u003C 0, 0, \"layer@1\")"," to sanitize outputs.",[236,1199,1200,1203,1204,1185,1207,1210],{},[239,1201,1202],{},"Path Resolution Issues:"," Standalone scripts fail when relative paths are used or when working directories change. Always resolve paths with ",[210,1205,1206],{},"os.path.abspath()",[210,1208,1209],{},"pathlib.Path"," before passing them to QGIS algorithms.",[236,1212,1213,1216,1217,1219,1220,1223,1224,1227,1228,46],{},[239,1214,1215],{},"Processing Registry Not Loaded:"," Running scripts outside the QGIS console without initializing ",[210,1218,212],{}," causes ",[210,1221,1222],{},"Algorithm not found"," errors. Always call ",[210,1225,1226],{},"QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())"," before invoking ",[210,1229,978],{},[202,1231,1233],{"id":1232},"workflow-integration-scaling","Workflow Integration & Scaling",[15,1235,1236,1237,1240,1241,1245,1246,1250],{},"Raster analysis rarely operates in isolation. Extracted features frequently feed into ",[19,1238,1239],{"href":29},"Vector Data Manipulation"," pipelines, where polygonization, buffering, and spatial joins transform continuous surfaces into discrete analytical units. When a workflow strings several raster steps together — clip, then reclassify, then compute statistics — model it explicitly as a sequence of ",[19,1242,1244],{"href":1243},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","chained processing algorithms"," so each output becomes the next step's input. For multi-scene or time-series datasets, wrap the core logic in a directory iterator and lean on ",[19,1247,1249],{"href":1248},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002F","batch processing with PyQGIS"," to parallelize execution across folders without duplicating code.",[202,1252,1254],{"id":1253},"validation-quality-assurance","Validation & Quality Assurance",[15,1256,1257,1258,1261,1262,1265,1266,1269],{},"Always verify outputs against known ground truth or reference datasets. Use ",[210,1259,1260],{},"gdalinfo -stats"," to confirm value ranges match expectations, and visually inspect layer rendering in QGIS to catch alignment or compression artifacts. Implement automated checks in your script: ",[19,1263,1264],{"href":44},"read the band statistics"," of each output, compare output dimensions, verify pixel spacing consistency, and assert that no unexpected null bands exist. Logging intermediate steps with Python's ",[210,1267,1268],{},"logging"," module ensures traceability when workflows scale to production environments or are handed off to team members.",[202,1271,1273],{"id":1272},"the-raster-data-model-in-pyqgis","The raster data model in PyQGIS",[15,1275,1276,1277,1280],{},"Vector work has one dominant object, ",[210,1278,1279],{},"QgsVectorLayer",", and everything hangs off it. Raster work is layered differently, and knowing which object owns what saves a great deal of hunting through the API.",[15,1282,1283],{},[50,1284,1287,1290,1293,1297,1305,1309,1316,1321,1326,1333,1338,1351,1355,1359,1371,1375],{"viewBox":1285,"role":53,"ariaLabel":1286,"xmlns":55},"0 0 760 262","The raster object model: a raster layer owns a data provider that reads blocks from the file, and a renderer that turns values into pixels, with band statistics sitting on the provider",[57,1288,1289],{},"What owns what in a raster layer",[61,1291,1292],{},"A QgsRasterLayer owns a QgsRasterDataProvider and a renderer. The provider knows the file: its bands, dimensions, nodata value and statistics, and it serves blocks of values. The renderer knows the appearance: which band maps to which colour and how values are shaded. Analysis reads through the provider; styling changes the renderer.",[65,1294],{"x":67,"y":67,"width":1295,"height":1296,"fill":70},"760","262",[72,1298,1299],{},[75,1300,1302],{"id":1301,"viewBox":78,"refX":79,"refY":80,"markerWidth":81,"markerHeight":81,"orient":82},"rastModelArrow",[84,1303],{"d":86,"fill":1304},"#0f766e",[99,1306,1308],{"x":69,"y":1307,"style":1050,"fill":104,"textAnchor":105},"26","Analysis goes through the provider; styling goes through the renderer",[65,1310],{"x":1311,"y":1312,"width":1313,"height":1314,"rx":94,"fill":1315,"stroke":1304,"style":97},"288","50","184","56","#26322d",[99,1317,111],{"x":69,"y":1318,"style":1319,"fill":1320,"textAnchor":105},"74","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","#d9f99d",[99,1322,1325],{"x":69,"y":1323,"style":1324,"fill":1320,"textAnchor":105},"94","text-anchor:middle;font-size:10.5px;font-family:sans-serif","the handle you hold",[65,1327],{"x":1328,"y":1329,"width":1330,"height":1331,"rx":94,"fill":95,"stroke":1332,"style":97},"16","136","330","106","#2563eb",[99,1334,1337],{"x":1335,"y":1336,"style":1319,"fill":1332,"textAnchor":105},"181","160","dataProvider()",[173,1339,1341,1344,1348],{"style":1340},"font-size:10.5px;font-family:sans-serif",[99,1342,1343],{"x":91,"y":1313,"fill":87},"bandCount() · xSize() · ySize()",[99,1345,1347],{"x":91,"y":1346,"fill":87},"206","sourceNoDataValue() · block()",[99,1349,1350],{"x":91,"y":147,"fill":87},"bandStatistics() · histogram()",[65,1352],{"x":1353,"y":1329,"width":1330,"height":1331,"rx":94,"fill":95,"stroke":1354,"style":97},"414","#b45309",[99,1356,1358],{"x":1357,"y":1336,"style":1319,"fill":1354,"textAnchor":105},"579","renderer()",[173,1360,1361,1365,1368],{"style":1340},[99,1362,1364],{"x":1363,"y":1313,"fill":87},"438","singleBandPseudoColor · multiBandColor",[99,1366,1367],{"x":1363,"y":1346,"fill":87},"the shader and its ramp",[99,1369,1370],{"x":1363,"y":147,"fill":87},"contrast enhancement · opacity",[177,1372],{"x1":1330,"y1":1331,"x2":1373,"y2":189,"stroke":1332,"style":1374},"220","stroke-width:2.5;marker-end:url(#rastModelArrow)",[177,1376],{"x1":1377,"y1":1331,"x2":1378,"y2":189,"stroke":1354,"style":1374},"430","540",[15,1380,1381,1382,1385],{},"The practical consequence is that most analysis questions are provider questions. Reading a pixel value, counting how many exceed a threshold, or discovering the nodata value all go through ",[210,1383,1384],{},"layer.dataProvider()","; none of them touch the renderer. Conversely, nothing you do to the renderer changes a single stored value — it only changes how those values are drawn, which is why a \"wrong\" raster that looks corrected after a styling change has not actually been corrected at all.",[15,1387,1388,1389,1392,1393,1396],{},"The provider serves data in ",[239,1390,1391],{},"blocks"," rather than pixel by pixel, and that is deliberate. Reading a 10 000 × 10 000 raster one pixel at a time through Python crosses the language boundary a hundred million times; reading it in blocks of a few hundred rows crosses it a few hundred times for the same data. Any loop that calls ",[210,1394,1395],{},"provider.sample()"," per pixel will be slower than the same work done with a Processing algorithm by two or three orders of magnitude — which is the single most important performance fact about raster work in PyQGIS.",[202,1398,1400],{"id":1399},"nodata-is-the-recurring-trap","Nodata is the recurring trap",[15,1402,1403,1404,1406,1407,1409],{},"Almost every wrong raster result traces back to nodata. A raster's fill value — often ",[210,1405,1188],{},", sometimes ",[210,1408,67],{},", occasionally left undeclared entirely — is a real number stored in real pixels, and every arithmetic operation will happily include it unless something excludes it explicitly.",[15,1411,1412,1413,1416,1417,1420,1421,1424],{},"The three states worth distinguishing are: a ",[239,1414,1415],{},"declared"," nodata value that the provider reports and honours; an ",[239,1418,1419],{},"undeclared"," fill value that is present in the data but that nothing knows about; and ",[239,1422,1423],{},"genuinely absent"," pixels outside the raster's extent. Only the first is handled for you.",[279,1426,1428],{"className":281,"code":1427,"language":283,"meta":284,"style":284},"from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"elevation\")[0]\nprovider = layer.dataProvider()\n\nfor band in range(1, provider.bandCount() + 1):\n    declared = provider.sourceHasNoDataValue(band)\n    value = provider.sourceNoDataValue(band) if declared else None\n    print(f\"band {band}: nodata declared={declared} value={value}\")\n    stats = provider.bandStatistics(band)\n    print(f\"  range {stats.minimumValue:.2f} … {stats.maximumValue:.2f}\")\n",[210,1429,1430,1441,1445,1466,1476,1480,1511,1521,1542,1584,1594],{"__ignoreMap":284},[288,1431,1432,1434,1436,1438],{"class":177,"line":290},[288,1433,312],{"class":293},[288,1435,315],{"class":297},[288,1437,294],{"class":293},[288,1439,1440],{"class":297}," QgsProject\n",[288,1442,1443],{"class":177,"line":301},[288,1444,360],{"emptyLinePlaceholder":359},[288,1446,1447,1450,1452,1455,1458,1461,1463],{"class":177,"line":309},[288,1448,1449],{"class":297},"layer ",[288,1451,380],{"class":293},[288,1453,1454],{"class":297}," QgsProject.instance().mapLayersByName(",[288,1456,1457],{"class":398},"\"elevation\"",[288,1459,1460],{"class":297},")[",[288,1462,67],{"class":386},[288,1464,1465],{"class":297},"]\n",[288,1467,1468,1471,1473],{"class":177,"line":323},[288,1469,1470],{"class":297},"provider ",[288,1472,380],{"class":293},[288,1474,1475],{"class":297}," layer.dataProvider()\n",[288,1477,1478],{"class":177,"line":329},[288,1479,360],{"emptyLinePlaceholder":359},[288,1481,1482,1485,1488,1491,1494,1496,1499,1502,1505,1508],{"class":177,"line":335},[288,1483,1484],{"class":293},"for",[288,1486,1487],{"class":297}," band ",[288,1489,1490],{"class":293},"in",[288,1492,1493],{"class":386}," range",[288,1495,597],{"class":297},[288,1497,1498],{"class":386},"1",[288,1500,1501],{"class":297},", provider.bandCount() ",[288,1503,1504],{"class":293},"+",[288,1506,1507],{"class":386}," 1",[288,1509,1510],{"class":297},"):\n",[288,1512,1513,1516,1518],{"class":177,"line":348},[288,1514,1515],{"class":297},"    declared ",[288,1517,380],{"class":293},[288,1519,1520],{"class":297}," provider.sourceHasNoDataValue(band)\n",[288,1522,1523,1526,1528,1531,1533,1536,1539],{"class":177,"line":356},[288,1524,1525],{"class":297},"    value ",[288,1527,380],{"class":293},[288,1529,1530],{"class":297}," provider.sourceNoDataValue(band) ",[288,1532,565],{"class":293},[288,1534,1535],{"class":297}," declared ",[288,1537,1538],{"class":293},"else",[288,1540,1541],{"class":386}," None\n",[288,1543,1544,1547,1549,1551,1554,1556,1559,1561,1564,1566,1568,1570,1573,1575,1578,1580,1582],{"class":177,"line":363},[288,1545,1546],{"class":386},"    print",[288,1548,597],{"class":297},[288,1550,616],{"class":293},[288,1552,1553],{"class":398},"\"band ",[288,1555,622],{"class":386},[288,1557,1558],{"class":297},"band",[288,1560,628],{"class":386},[288,1562,1563],{"class":398},": nodata declared=",[288,1565,622],{"class":386},[288,1567,1415],{"class":297},[288,1569,628],{"class":386},[288,1571,1572],{"class":398}," value=",[288,1574,622],{"class":386},[288,1576,1577],{"class":297},"value",[288,1579,628],{"class":386},[288,1581,651],{"class":398},[288,1583,332],{"class":297},[288,1585,1586,1589,1591],{"class":177,"line":370},[288,1587,1588],{"class":297},"    stats ",[288,1590,380],{"class":293},[288,1592,1593],{"class":297}," provider.bandStatistics(band)\n",[288,1595,1596,1598,1600,1602,1605,1607,1610,1613,1615,1618,1620,1623,1625,1627,1629],{"class":177,"line":415},[288,1597,1546],{"class":386},[288,1599,597],{"class":297},[288,1601,616],{"class":293},[288,1603,1604],{"class":398},"\"  range ",[288,1606,622],{"class":386},[288,1608,1609],{"class":297},"stats.minimumValue",[288,1611,1612],{"class":293},":.2f",[288,1614,628],{"class":386},[288,1616,1617],{"class":398}," … ",[288,1619,622],{"class":386},[288,1621,1622],{"class":297},"stats.maximumValue",[288,1624,1612],{"class":293},[288,1626,628],{"class":386},[288,1628,651],{"class":398},[288,1630,332],{"class":297},[15,1632,1633,1636,1637,1640,1641,1645,1646,1649,1650,1652],{},[239,1634,1635],{},"Breakdown:"," ",[210,1638,1639],{},"sourceHasNoDataValue()"," reports whether the ",[1642,1643,1644],"em",{},"file"," declares a nodata value, which is different from whether QGIS is currently treating one as nodata — a user or an earlier script can add one at layer level with ",[210,1647,1648],{},"setUseSourceNoDataValue()",". Printing the statistics alongside is the giveaway: a minimum of ",[210,1651,1188],{}," on an elevation raster whose real floor is sea level tells you immediately that the fill value is being counted as data. That one comparison catches the problem before it propagates into a mean, a classification or a hillshade.",[15,1654,1655],{},"When a fill value turns out to be undeclared, declaring it is a metadata change rather than a data change and is therefore cheap and reversible:",[279,1657,1659],{"className":281,"code":1658,"language":283,"meta":284,"style":284},"provider.setNoDataValue(1, -9999)\nlayer.triggerRepaint()\n",[210,1660,1661,1676],{"__ignoreMap":284},[288,1662,1663,1666,1668,1670,1672,1674],{"class":177,"line":290},[288,1664,1665],{"class":297},"provider.setNoDataValue(",[288,1667,1498],{"class":386},[288,1669,390],{"class":297},[288,1671,747],{"class":293},[288,1673,750],{"class":386},[288,1675,332],{"class":297},[288,1677,1678],{"class":177,"line":301},[288,1679,1680],{"class":297},"layer.triggerRepaint()\n",[15,1682,1683,1636,1685,1688,1689,1692],{},[239,1684,1635],{},[210,1686,1687],{},"setNoDataValue()"," writes the declaration into the file where the format supports it, so every later read — by QGIS, GDAL or anything else — excludes those pixels. On a format that cannot store it, set it at layer level with ",[210,1690,1691],{},"layer.setUseSourceNoDataValue()"," and an additional nodata range instead, accepting that the setting travels with the project rather than the data.",[202,1694,1696],{"id":1695},"key-takeaways","Key Takeaways",[971,1698,1699,1708,1715,1725,1735],{},[236,1700,1701,1702,1704,1705,1707],{},"Drive every step through ",[210,1703,978],{},"; the Processing framework manages tiling, block reads, and nodata handling that manual ",[210,1706,982],{}," bindings leave to you.",[236,1709,1710,1711,1714],{},"Align coordinate reference systems, pixel spacing, and extents ",[1642,1712,1713],{},"before"," you compute — misalignment corrupts results silently rather than raising an error.",[236,1716,1717,1718,1721,1722,1724],{},"Reference bands with the 1-based ",[210,1719,1720],{},"\"layer@N\""," syntax inside calculator expressions, and define ",[210,1723,1192],{}," explicitly to keep null values from poisoning your output.",[236,1726,1727,1728,1731,1732,1734],{},"Write intermediate results to disk (or use VRTs) rather than holding large rasters in memory, and enable ",[210,1729,1730],{},"MULTITHREADING"," to avoid ",[210,1733,1170],{}," failures.",[236,1736,1737,1738,1740,1741,1743],{},"In standalone scripts, register native algorithms with ",[210,1739,1226],{}," before the first ",[210,1742,978],{}," call, then validate outputs against reference data before shipping.",[202,1745,1747],{"id":1746},"frequently-asked-questions","Frequently Asked Questions",[15,1749,1750,1759,1760,1762,1763,1765],{},[239,1751,1752,1753,1755,1756,1758],{},"Why should I use ",[210,1754,978],{}," instead of the ",[210,1757,982],{}," class directly?","\nThe Processing framework handles block-wise reading, tiling, and nodata propagation automatically, which keeps memory usage bounded on large rasters. The legacy ",[210,1761,982],{}," bindings require manual block iteration and are easy to misuse on datasets that do not fit in RAM. For reproducible, scriptable pipelines, ",[210,1764,1012],{}," is the recommended entry point in QGIS 3.34.",[15,1767,1768,1771,1772,1775,1776,1779,1780,1783,1784,1787],{},[239,1769,1770],{},"How do I reference individual bands inside a raster calculator expression?","\nUse the ",[210,1773,1774],{},"\"layername@N\""," syntax, where ",[210,1777,1778],{},"N"," is the 1-based band index, for example ",[210,1781,1782],{},"\"input@1\""," for the first band or ",[210,1785,1786],{},"\"input@4\""," for the fourth. Band indices start at 1, not 0, and the layer name must match the loaded layer or the resolved file path. Mismatched names produce an empty or invalid output rather than an explicit error.",[15,1789,1790,1793,1794,1796,1797,41,1799,1801,1802,1804],{},[239,1791,1792],{},"My clipped output is shifted or blank — what went wrong?","\nThis almost always means the raster and the mask vector are in different coordinate reference systems. ",[210,1795,1156],{}," does not reproject on the fly, so pass matching ",[210,1798,989],{},[210,1800,992],{},", or reproject the mask first. Confirm both EPSG codes with ",[210,1803,226],{}," before clipping.",[15,1806,1807,1813,1814,1816],{},[239,1808,1809,1810,1812],{},"How do I avoid ",[210,1811,1170],{}," memory errors on very large rasters?","\nEnable ",[210,1815,1174],{},", write intermediate results to disk instead of holding them in memory, and consider building a Virtual Raster (VRT) so GDAL streams tiles rather than loading the full dataset. Reducing block size and freeing temporary layers between steps also helps on constrained hardware.",[15,1818,1819,1822,1823,1825,1826,1829,1830,1832],{},[239,1820,1821],{},"Do I need to register native algorithms when running outside the QGIS console?","\nYes. In a standalone script you must call ",[210,1824,1226],{}," after ",[210,1827,1828],{},"initQgis()",", otherwise ",[210,1831,978],{}," raises an \"Algorithm not found\" error. Inside the QGIS Python console this registration has already happened for you.",[15,1834,1835,1838,1839,1842],{},[239,1836,1837],{},"Why is my raster loop so slow?","\nAlmost certainly a per-pixel read. Every ",[210,1840,1841],{},"sample()"," call crosses from Python into C++, so a ten-thousand-square raster means a hundred million crossings. Read in blocks, or better, use a Processing algorithm that does the whole operation in compiled code.",[15,1844,1845,1848,1849,1406,1851,1853],{},[239,1846,1847],{},"What is the nodata value and why does it matter?","\nIt is the number stored in pixels that hold no measurement — often ",[210,1850,1188],{},[210,1852,67],{},". It is a real value in real pixels, so unless it is declared and honoured it is included in every mean, histogram and classification you compute, silently skewing the result.",[15,1855,1856,1859,1862,1863,1866,1867,1869],{},[239,1857,1858],{},"How do I check whether my raster declares a nodata value?",[210,1860,1861],{},"provider.sourceHasNoDataValue(band)"," reports whether the file declares one, and ",[210,1864,1865],{},"sourceNoDataValue(band)"," gives it. Comparing that against the band's reported minimum is the quickest sanity check: a minimum of ",[210,1868,1188],{}," on an elevation raster means the fill value is being counted as data.",[15,1871,1872,1875],{},[239,1873,1874],{},"Does changing the renderer change my raster values?","\nNo. The renderer only decides how stored values are drawn. A raster that looks corrected after a styling change has not been corrected at all, which matters when the next step reads the values rather than the pixels.",[15,1877,1878,1881],{},[239,1879,1880],{},"Should I use a Processing algorithm or the raster API?","\nThe algorithm, unless it genuinely cannot express what you need. Algorithms run in compiled code, handle blocks, nodata and progress reporting for you, and are far harder to get subtly wrong than a hand-written loop.",[15,1883,1884,1887,1890],{},[239,1885,1886],{},"How do I read a single pixel value at a coordinate?",[210,1888,1889],{},"provider.sample(QgsPointXY(x, y), band)"," returns the value and a validity flag. It is the right tool for a handful of points and entirely the wrong one for a whole raster, where a per-pixel loop is orders of magnitude slower than a Processing algorithm.",[15,1892,1893,1896],{},[239,1894,1895],{},"Why do my zonal statistics include impossible values?","\nAlmost always nodata being counted as data. A polygon overlapping the edge of the raster averages real measurements against the fill value, producing a mean that sits somewhere between the two and belongs to neither.",[15,1898,1899,1902,1903,1906],{},[239,1900,1901],{},"Can I write raster values back from Python?","\nYou can, through ",[210,1904,1905],{},"QgsRasterFileWriter"," and a block-based loop, but for anything expressible as an algorithm the raster calculator or a GDAL algorithm will be dramatically faster and far less error-prone.",[15,1908,1909,1912,1915,1916,1919],{},[239,1910,1911],{},"How do I find out what bands a raster has?",[210,1913,1914],{},"provider.bandCount()"," gives the number and ",[210,1917,1918],{},"layer.bandName(n)"," gives each name where the format stores one. For a multispectral image the band order is part of the product specification rather than something QGIS can infer, so check the source documentation before assuming which band is which.",[15,1921,1922,1925],{},[239,1923,1924],{},"Does clipping a raster reduce its file size proportionally?","\nNot necessarily. Cropping alone keeps the corner pixels, and an uncompressed output can be larger than a compressed input covering more ground. Enable compression and tiling on the output when size matters.",[15,1927,1928,1931],{},[239,1929,1930],{},"Can I analyse a raster larger than available memory?","\nYes, provided you never load it all at once. Processing algorithms stream in blocks, so they handle rasters far larger than RAM; a Python loop that materialises the whole array does not.",[202,1933,1935],{"id":1934},"related-guides","Related Guides",[971,1937,1938,1944,1949,1954,1959,1964,1969,1974,1980],{},[236,1939,1940,1941,1943],{},"Up: ",[19,1942,22],{"href":21}," — the parent guide for this workflow",[236,1945,1946],{},[19,1947,1948],{"href":29},"Vector Data Manipulation in PyQGIS",[236,1950,1951],{},[19,1952,1953],{"href":34},"Coordinate Reference Systems in PyQGIS",[236,1955,1956],{},[19,1957,1958],{"href":1243},"Chaining Processing Algorithms in PyQGIS",[236,1960,1961],{},[19,1962,1963],{"href":1248},"Batch Processing with PyQGIS",[236,1965,1966],{},[19,1967,1968],{"href":44},"Calculate Raster Statistics in PyQGIS",[236,1970,1971],{},[19,1972,1973],{"href":39},"Clip a Raster by a Mask Layer in PyQGIS",[236,1975,1976],{},[19,1977,1979],{"href":1978},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fraster-calculator-pyqgis\u002F","Use the Raster Calculator in PyQGIS",[236,1981,1982],{},[19,1983,1985],{"href":1984},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002Fsample-raster-values-at-points-pyqgis\u002F","Sample Raster Values at Points in PyQGIS",[1987,1988,1989],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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);}",{"title":284,"searchDepth":301,"depth":301,"links":1991},[1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002],{"id":204,"depth":301,"text":205},{"id":230,"depth":301,"text":231},{"id":269,"depth":301,"text":270},{"id":1144,"depth":301,"text":1145},{"id":1232,"depth":301,"text":1233},{"id":1253,"depth":301,"text":1254},{"id":1272,"depth":301,"text":1273},{"id":1399,"depth":301,"text":1400},{"id":1695,"depth":301,"text":1696},{"id":1746,"depth":301,"text":1747},{"id":1934,"depth":301,"text":1935},"Learn raster analysis workflows in QGIS with PyQGIS. Covers band math, zonal statistics, terrain analysis, and raster-to-vector conversion.","md",{"slug":2006,"type":2007,"breadcrumb":2008,"datePublished":2009,"dateModified":2010},"raster-analysis-workflows","guide","Raster Analysis Workflows","2025-03-11","2026-07-18","\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows",{"title":5,"description":2003},"spatial-data-processing-automation\u002Fraster-analysis-workflows\u002Findex","InxzKsfRUXO6zDQ_lMT30voUuIJAe64jjklaU7djDj4",1787823363765]