[{"data":1,"prerenderedAt":1784},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Frun-pdal-algorithms-from-pyqgis":3},{"id":4,"title":5,"body":6,"description":1771,"extension":1772,"meta":1773,"navigation":277,"path":1774,"seo":1775,"stem":1782,"__hash__":1783},"docs\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Frun-pdal-algorithms-from-pyqgis\u002Findex.md","Run the PDAL Algorithms from PyQGIS",{"type":7,"value":8,"toc":1757},"minimark",[9,13,26,39,206,211,237,241,244,363,381,385,391,420,426,506,533,537,837,864,868,881,990,1179,1195,1199,1202,1411,1427,1430,1434,1439,1557,1570,1574,1588,1592,1656,1660,1666,1670,1683,1695,1705,1719,1723,1753],[10,11,5],"h1",{"id":12},"run-the-pdal-algorithms-from-pyqgis",[14,15,16,17,21,22,25],"p",{},"QGIS wraps PDAL as an ordinary Processing provider, which means every LiDAR operation is a ",[18,19,20],"code",{},"processing.run()"," call with a dictionary of parameters — the same shape as a buffer or a clip. That is a much better deal than it sounds. You get progress reporting, cancellation, temporary output handling and a stable identifier that survives version upgrades, none of which you get from shelling out to the ",[18,23,24],{},"pdal"," binary yourself.",[14,27,28,29,34,35,38],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002F","Point Cloud & LiDAR Workflows in PyQGIS",". It covers finding out what your build actually ships, the parameter conventions that differ from the ",[18,36,37],{},"native:"," algorithms, the expression syntax trap, and how to chain several steps without writing intermediate files to disk.",[14,40,41],{},[42,43,48,52,56,63,72,82,88,95,99,103,109,113,117,121,125,128,131,134,137,142,146,150,153,156,159,162,168,172,176,179,182,185,188,197,201],"svg",{"viewBox":44,"role":45,"ariaLabel":46,"xmlns":47},"0 0 760 342","img","The PDAL algorithm family grouped into four jobs: inspecting a file, converting format or projection, reducing the number of points, and exporting to raster or vector","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[49,50,51],"title",{},"The pdal: family, grouped by what it is for",[53,54,55],"desc",{},"The wrapped PDAL algorithms fall into four groups. Inspection reports what is in a file. Conversion changes format, projection or index. Reduction cuts the number of points by filtering, thinning, clipping or tiling. Export turns points into raster or vector products that the rest of QGIS understands.",[57,58],"rect",{"x":59,"y":59,"width":60,"height":61,"fill":62},"0","760","342","#f6f3ea",[64,65,71],"text",{"x":66,"y":67,"style":68,"fill":69,"textAnchor":70},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Four jobs, eighteen algorithms",[57,73],{"x":74,"y":75,"width":76,"height":77,"rx":78,"fill":79,"stroke":80,"style":81},"20","46","172","188","10","#eff3ff","#2563eb","stroke-width:2.5",[64,83,87],{"x":84,"y":85,"style":86,"fill":80,"textAnchor":70},"106","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","inspect",[64,89,94],{"x":90,"y":91,"style":92,"fill":93},"36","100","font-size:10.5px;font-family:sans-serif","#2f3b35","pdal:info",[64,96,98],{"x":90,"y":97,"style":92,"fill":93},"124","pdal:boundary",[64,100,102],{"x":90,"y":101,"style":92,"fill":93},"148","pdal:density",[64,104,108],{"x":90,"y":105,"style":106,"fill":107},"186","font-size:10px;font-family:sans-serif","#59645f","answers what is",[64,110,112],{"x":90,"y":111,"style":106,"fill":107},"204","in the file, cheaply",[57,114],{"x":111,"y":75,"width":76,"height":77,"rx":78,"fill":115,"stroke":116,"style":81},"#eef7f4","#0f766e",[64,118,120],{"x":119,"y":85,"style":86,"fill":116,"textAnchor":70},"290","convert",[64,122,124],{"x":123,"y":91,"style":92,"fill":93},"220","pdal:convertformat",[64,126,127],{"x":123,"y":97,"style":92,"fill":93},"pdal:createcopc",[64,129,130],{"x":123,"y":101,"style":92,"fill":93},"pdal:reproject",[64,132,133],{"x":123,"y":76,"style":92,"fill":93},"pdal:assignprojection",[64,135,136],{"x":123,"y":111,"style":106,"fill":107},"run once, keep the result",[57,138],{"x":139,"y":75,"width":76,"height":77,"rx":78,"fill":140,"stroke":141,"style":81},"388","#fdf2e2","#b45309",[64,143,145],{"x":144,"y":85,"style":86,"fill":141,"textAnchor":70},"474","reduce",[64,147,149],{"x":148,"y":91,"style":92,"fill":93},"404","pdal:filter",[64,151,152],{"x":148,"y":97,"style":92,"fill":93},"pdal:clip",[64,154,155],{"x":148,"y":101,"style":92,"fill":93},"pdal:thinbyradius",[64,157,158],{"x":148,"y":76,"style":92,"fill":93},"pdal:tile · pdal:merge",[64,160,161],{"x":148,"y":111,"style":106,"fill":107},"the expensive steps",[57,163],{"x":164,"y":75,"width":165,"height":77,"rx":78,"fill":166,"stroke":167,"style":81},"572","168","#edf8e9","#15803d",[64,169,171],{"x":170,"y":85,"style":86,"fill":167,"textAnchor":70},"656","export",[64,173,175],{"x":174,"y":91,"style":92,"fill":93},"588","pdal:exportraster",[64,177,178],{"x":174,"y":97,"style":92,"fill":93},"pdal:exportrastertin",[64,180,181],{"x":174,"y":101,"style":92,"fill":93},"pdal:exportvector",[64,183,184],{"x":174,"y":105,"style":106,"fill":107},"hands over to the",[64,186,187],{"x":174,"y":111,"style":106,"fill":107},"rest of QGIS",[57,189],{"x":190,"y":191,"width":192,"height":193,"rx":194,"fill":195,"stroke":116,"style":196},"80","260","600","62","8","#fffdf7","stroke-width:2",[64,198,200],{"x":66,"y":199,"style":86,"fill":116,"textAnchor":70},"284","a typical pipeline touches one from each group, in that order",[64,202,205],{"x":66,"y":203,"style":204,"fill":93,"textAnchor":70},"308","text-anchor:middle;font-size:10.5px;font-family:sans-serif","info → createcopc → clip and filter → exportraster",[207,208,210],"h2",{"id":209},"prerequisites","Prerequisites",[212,213,214,222,230],"ul",{},[215,216,217,221],"li",{},[218,219,220],"strong",{},"QGIS 3.34 LTR"," or newer. The PDAL provider arrived in 3.32; virtual point cloud support in 3.34.",[215,223,224,225,229],{},"Processing initialised. Inside QGIS it already is; in a standalone script see ",[30,226,228],{"href":227},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","running Python scripts outside QGIS Desktop",".",[215,231,232,233,229],{},"An indexed point cloud, as covered in ",[30,234,236],{"href":235},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fload-point-cloud-layer-pyqgis\u002F","loading a point cloud layer",[207,238,240],{"id":239},"find-out-what-your-build-ships","Find out what your build ships",[14,242,243],{},"Algorithm availability varies with the QGIS version and, occasionally, with how a distribution packaged PDAL. Rather than trusting a list in an article, ask the registry.",[245,246,251],"pre",{"className":247,"code":248,"language":249,"meta":250,"style":250},"language-python shiki shiki-themes github-dark","from qgis.core import QgsApplication\n\nregistry = QgsApplication.processingRegistry()\nfor alg in registry.algorithms():\n    if alg.id().startswith(\"pdal:\"):\n        print(f\"{alg.id():34} {alg.displayName()}\")\n","python","",[18,252,253,272,279,291,306,322],{"__ignoreMap":250},[254,255,258,262,266,269],"span",{"class":256,"line":257},"line",1,[254,259,261],{"class":260},"snl16","from",[254,263,265],{"class":264},"s95oV"," qgis.core ",[254,267,268],{"class":260},"import",[254,270,271],{"class":264}," QgsApplication\n",[254,273,275],{"class":256,"line":274},2,[254,276,278],{"emptyLinePlaceholder":277},true,"\n",[254,280,282,285,288],{"class":256,"line":281},3,[254,283,284],{"class":264},"registry ",[254,286,287],{"class":260},"=",[254,289,290],{"class":264}," QgsApplication.processingRegistry()\n",[254,292,294,297,300,303],{"class":256,"line":293},4,[254,295,296],{"class":260},"for",[254,298,299],{"class":264}," alg ",[254,301,302],{"class":260},"in",[254,304,305],{"class":264}," registry.algorithms():\n",[254,307,309,312,315,319],{"class":256,"line":308},5,[254,310,311],{"class":260},"    if",[254,313,314],{"class":264}," alg.id().startswith(",[254,316,318],{"class":317},"sU2Wk","\"pdal:\"",[254,320,321],{"class":264},"):\n",[254,323,325,329,332,335,338,341,344,347,350,353,356,358,360],{"class":256,"line":324},6,[254,326,328],{"class":327},"sDLfK","        print",[254,330,331],{"class":264},"(",[254,333,334],{"class":260},"f",[254,336,337],{"class":317},"\"",[254,339,340],{"class":327},"{",[254,342,343],{"class":264},"alg.id()",[254,345,346],{"class":260},":34",[254,348,349],{"class":327},"}",[254,351,352],{"class":327}," {",[254,354,355],{"class":264},"alg.displayName()",[254,357,349],{"class":327},[254,359,337],{"class":317},[254,361,362],{"class":264},")\n",[14,364,365,368,369,372,373,376,377,380],{},[218,366,367],{},"Breakdown:"," ",[18,370,371],{},"algorithms()"," returns every registered algorithm across every provider, so filtering on the identifier prefix is the cheapest way to enumerate one family. If this prints nothing, Processing has not been initialised (in a standalone script you must call ",[18,374,375],{},"Processing.initialize()"," yourself) or the build has no PDAL provider. To go one level deeper on a single algorithm, ",[18,378,379],{},"registry.algorithmById(\"pdal:exportraster\").parameterDefinitions()"," lists every parameter with its name, type and default — which is more reliable than any documentation, because it is the code.",[207,382,384],{"id":383},"the-parameter-conventions-that-differ","The parameter conventions that differ",[14,386,387,388,390],{},"Three conventions in this family will catch you out if you are used to the ",[18,389,37],{}," algorithms.",[14,392,393,399,400,402,403,406,407,410,411,414,415,419],{},[218,394,395,398],{},[18,396,397],{},"INPUT"," takes a path or a layer."," Most ",[18,401,37],{}," algorithms want a ",[18,404,405],{},"QgsMapLayer"," or a source string; the PDAL ones accept a ",[18,408,409],{},"QgsPointCloudLayer",", a file path, or a ",[18,412,413],{},".vpc"," manifest, and resolve all three to a file on disk. That is convenient, and it means a layer with a subset string applied has that subset ",[416,417,418],"em",{},"ignored"," — the algorithm reads the file, not your view of it.",[14,421,422,425],{},[218,423,424],{},"Filter expressions are PDAL syntax."," This is the single biggest trap in the family.",[14,427,428],{},[42,429,432,435,438,441,444,449,452,457,462,466,470,474,477,481,484,488,491,494,497,501],{"viewBox":430,"role":45,"ariaLabel":431,"xmlns":47},"0 0 760 300","A side-by-side comparison of the QGIS expression syntax used by a layer subset string and the PDAL expression syntax used by algorithm filter parameters",[49,433,434],{},"Two expression languages, one attribute name",[53,436,437],{},"A point cloud layer's subset string is QGIS expression syntax with single equals, IN lists and AND. A PDAL algorithm's filter expression uses doubled equals, doubled ampersands and no IN operator. The same attribute names appear in both, which is why the mistake is so easy to make and so hard to see.",[57,439],{"x":59,"y":59,"width":60,"height":440,"fill":62},"300",[64,442,443],{"x":66,"y":67,"style":68,"fill":69,"textAnchor":70},"The same filter, written two different ways",[57,445],{"x":446,"y":75,"width":447,"height":448,"rx":78,"fill":115,"stroke":116,"style":81},"24","344","196",[64,450,451],{"x":448,"y":85,"style":86,"fill":116,"textAnchor":70},"layer.setSubsetString(...)",[64,453,456],{"x":448,"y":454,"style":455,"fill":107,"textAnchor":70},"92","text-anchor:middle;font-size:10px;font-family:sans-serif","QGIS expression syntax",[64,458,461],{"x":459,"y":97,"style":460,"fill":93},"44","font-size:11px;font-family:monospace","Classification = 2",[64,463,465],{"x":459,"y":464,"style":460,"fill":93},"150","Classification IN (2, 9)",[64,467,469],{"x":459,"y":468,"style":460,"fill":93},"176","Z > 40 AND Intensity \u003C 900",[64,471,473],{"x":459,"y":472,"style":106,"fill":107},"212","a view — the file is unchanged",[57,475],{"x":476,"y":75,"width":447,"height":448,"rx":78,"fill":140,"stroke":141,"style":81},"392",[64,478,480],{"x":479,"y":85,"style":86,"fill":141,"textAnchor":70},"564","FILTER_EXPRESSION",[64,482,483],{"x":479,"y":454,"style":455,"fill":107,"textAnchor":70},"PDAL expression syntax",[64,485,487],{"x":486,"y":97,"style":460,"fill":93},"412","Classification == 2",[64,489,490],{"x":486,"y":464,"style":460,"fill":93},"Classification == 2 || Classification == 9",[64,492,493],{"x":486,"y":468,"style":460,"fill":93},"Z > 40 && Intensity \u003C 900",[64,495,496],{"x":486,"y":472,"style":106,"fill":107},"decides what is written to the output",[57,498],{"x":190,"y":499,"width":192,"height":90,"rx":194,"fill":195,"stroke":500,"style":196},"256","#b91c1c",[64,502,505],{"x":66,"y":503,"style":504,"fill":500,"textAnchor":70},"280","text-anchor:middle;font-size:11px;font-family:sans-serif","a single equals in the right-hand column matches nothing and raises nothing",[14,507,508,368,511,514,515,517,518,520,521,524,525,528,529,532],{},[218,509,510],{},"Outputs are paths, and the extension chooses the driver.",[18,512,513],{},"OUTPUT"," on ",[18,516,175],{}," writes whatever GDAL infers from the extension; on ",[18,519,149],{}," it writes a point cloud whose format follows the extension too, so ",[18,522,523],{},".copc.laz"," gives you an indexed result and ",[18,526,527],{},".laz"," gives you an unindexed one. ",[18,530,531],{},"TEMPORARY_OUTPUT"," works but produces a file in the temporary folder rather than an in-memory layer, because there is no such thing as a memory point cloud.",[207,534,536],{"id":535},"a-single-call-with-feedback","A single call, with feedback",[245,538,540],{"className":247,"code":539,"language":249,"meta":250,"style":250},"import processing\nfrom qgis.core import QgsProcessingFeedback\n\n\nclass Progress(QgsProcessingFeedback):\n    def setProgress(self, value):\n        print(f\"\\r{value:5.1f}%\", end=\"\", flush=True)\n\n    def pushInfo(self, info):\n        print(f\"\\n{info}\")\n\n\nfeedback = Progress()\n\nresult = processing.run(\"pdal:exportraster\", {\n    \"INPUT\": \"\u002Fdata\u002Findexed\u002Fsurvey.vpc\",\n    \"RESOLUTION\": 1.0,\n    \"TILE_SIZE\": 1000,\n    \"FILTER_EXPRESSION\": \"Classification == 2\",\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fdtm.tif\",\n}, feedback=feedback)\n\nprint(\"\\nwrote\", result[\"OUTPUT\"])\n",[18,541,542,549,560,564,568,584,595,644,649,660,683,688,693,704,709,726,741,754,767,780,793,807,812],{"__ignoreMap":250},[254,543,544,546],{"class":256,"line":257},[254,545,268],{"class":260},[254,547,548],{"class":264}," processing\n",[254,550,551,553,555,557],{"class":256,"line":274},[254,552,261],{"class":260},[254,554,265],{"class":264},[254,556,268],{"class":260},[254,558,559],{"class":264}," QgsProcessingFeedback\n",[254,561,562],{"class":256,"line":281},[254,563,278],{"emptyLinePlaceholder":277},[254,565,566],{"class":256,"line":293},[254,567,278],{"emptyLinePlaceholder":277},[254,569,570,573,577,579,582],{"class":256,"line":308},[254,571,572],{"class":260},"class",[254,574,576],{"class":575},"svObZ"," Progress",[254,578,331],{"class":264},[254,580,581],{"class":575},"QgsProcessingFeedback",[254,583,321],{"class":264},[254,585,586,589,592],{"class":256,"line":324},[254,587,588],{"class":260},"    def",[254,590,591],{"class":575}," setProgress",[254,593,594],{"class":264},"(self, value):\n",[254,596,598,600,602,604,606,609,612,615,617,620,623,627,629,632,634,637,639,642],{"class":256,"line":597},7,[254,599,328],{"class":327},[254,601,331],{"class":264},[254,603,334],{"class":260},[254,605,337],{"class":317},[254,607,608],{"class":327},"\\r{",[254,610,611],{"class":264},"value",[254,613,614],{"class":260},":5.1f",[254,616,349],{"class":327},[254,618,619],{"class":317},"%\"",[254,621,622],{"class":264},", ",[254,624,626],{"class":625},"s9osk","end",[254,628,287],{"class":260},[254,630,631],{"class":317},"\"\"",[254,633,622],{"class":264},[254,635,636],{"class":625},"flush",[254,638,287],{"class":260},[254,640,641],{"class":327},"True",[254,643,362],{"class":264},[254,645,647],{"class":256,"line":646},8,[254,648,278],{"emptyLinePlaceholder":277},[254,650,652,654,657],{"class":256,"line":651},9,[254,653,588],{"class":260},[254,655,656],{"class":575}," pushInfo",[254,658,659],{"class":264},"(self, info):\n",[254,661,663,665,667,669,671,674,677,679,681],{"class":256,"line":662},10,[254,664,328],{"class":327},[254,666,331],{"class":264},[254,668,334],{"class":260},[254,670,337],{"class":317},[254,672,673],{"class":327},"\\n{",[254,675,676],{"class":264},"info",[254,678,349],{"class":327},[254,680,337],{"class":317},[254,682,362],{"class":264},[254,684,686],{"class":256,"line":685},11,[254,687,278],{"emptyLinePlaceholder":277},[254,689,691],{"class":256,"line":690},12,[254,692,278],{"emptyLinePlaceholder":277},[254,694,696,699,701],{"class":256,"line":695},13,[254,697,698],{"class":264},"feedback ",[254,700,287],{"class":260},[254,702,703],{"class":264}," Progress()\n",[254,705,707],{"class":256,"line":706},14,[254,708,278],{"emptyLinePlaceholder":277},[254,710,712,715,717,720,723],{"class":256,"line":711},15,[254,713,714],{"class":264},"result ",[254,716,287],{"class":260},[254,718,719],{"class":264}," processing.run(",[254,721,722],{"class":317},"\"pdal:exportraster\"",[254,724,725],{"class":264},", {\n",[254,727,729,732,735,738],{"class":256,"line":728},16,[254,730,731],{"class":317},"    \"INPUT\"",[254,733,734],{"class":264},": ",[254,736,737],{"class":317},"\"\u002Fdata\u002Findexed\u002Fsurvey.vpc\"",[254,739,740],{"class":264},",\n",[254,742,744,747,749,752],{"class":256,"line":743},17,[254,745,746],{"class":317},"    \"RESOLUTION\"",[254,748,734],{"class":264},[254,750,751],{"class":327},"1.0",[254,753,740],{"class":264},[254,755,757,760,762,765],{"class":256,"line":756},18,[254,758,759],{"class":317},"    \"TILE_SIZE\"",[254,761,734],{"class":264},[254,763,764],{"class":327},"1000",[254,766,740],{"class":264},[254,768,770,773,775,778],{"class":256,"line":769},19,[254,771,772],{"class":317},"    \"FILTER_EXPRESSION\"",[254,774,734],{"class":264},[254,776,777],{"class":317},"\"Classification == 2\"",[254,779,740],{"class":264},[254,781,783,786,788,791],{"class":256,"line":782},20,[254,784,785],{"class":317},"    \"OUTPUT\"",[254,787,734],{"class":264},[254,789,790],{"class":317},"\"\u002Fdata\u002Foutput\u002Fdtm.tif\"",[254,792,740],{"class":264},[254,794,796,799,802,804],{"class":256,"line":795},21,[254,797,798],{"class":264},"}, ",[254,800,801],{"class":625},"feedback",[254,803,287],{"class":260},[254,805,806],{"class":264},"feedback)\n",[254,808,810],{"class":256,"line":809},22,[254,811,278],{"emptyLinePlaceholder":277},[254,813,815,818,820,822,825,828,831,834],{"class":256,"line":814},23,[254,816,817],{"class":327},"print",[254,819,331],{"class":264},[254,821,337],{"class":317},[254,823,824],{"class":327},"\\n",[254,826,827],{"class":317},"wrote\"",[254,829,830],{"class":264},", result[",[254,832,833],{"class":317},"\"OUTPUT\"",[254,835,836],{"class":264},"])\n",[14,838,839,841,842,844,845,847,848,851,852,855,856,859,860,229],{},[218,840,367],{}," Subclassing ",[18,843,581],{}," is the supported way to see inside a long job; the base class does nothing, and the default ",[18,846,20],{}," feedback prints to the QGIS log rather than your console. ",[18,849,850],{},"pushInfo"," carries the messages PDAL itself emits, including the stage names, which is how you find out that a job is spending its time reprojecting rather than gridding. The same object exposes ",[18,853,854],{},"cancel()"," and ",[18,857,858],{},"isCanceled()",", which is what makes a plugin's stop button work — see ",[30,861,863],{"href":862},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Freport-progress-and-cancel-processing-algorithm\u002F","reporting progress and cancelling a processing algorithm",[207,865,867],{"id":866},"chaining-without-littering-the-disk","Chaining without littering the disk",[14,869,870,871,622,874,877,878,880],{},"A real pipeline is several steps, and writing every intermediate to a named file leaves you with a folder of ",[18,872,873],{},"step1.laz",[18,875,876],{},"step2.laz"," files that nobody dares delete. Chaining through ",[18,879,531],{}," keeps the mess inside the session.",[14,882,883],{},[42,884,887,890,893,896,911,914,920,924,928,934,937,940,943,946,949,951,954,958,961,964,967,972,976,978,981,987],{"viewBox":885,"role":45,"ariaLabel":886,"xmlns":47},"0 0 760 288","A four-stage point cloud pipeline where the first three stages write temporary outputs that are passed straight into the next stage and only the final raster is written to a named path",[49,888,889],{},"Only the last step gets a name",[53,891,892],{},"Reproject, clip and thin each write a temporary point cloud that is fed directly into the next stage. Only the final export names a file on disk, so the intermediate products are cleaned up with the session and never accumulate in the output folder.",[57,894],{"x":59,"y":59,"width":60,"height":895,"fill":62},"288",[897,898,899],"defs",{},[900,901,907],"marker",{"id":902,"viewBox":903,"refX":194,"refY":904,"markerWidth":905,"markerHeight":905,"orient":906},"pdalChainArrow","0 0 10 10","5","7","auto-start-reverse",[908,909],"path",{"d":910,"fill":93},"M0 0 L10 5 L0 10 z",[64,912,913],{"x":66,"y":67,"style":68,"fill":69,"textAnchor":70},"Four stages, one file at the end",[57,915],{"x":916,"y":917,"width":918,"height":919,"rx":194,"fill":115,"stroke":116,"style":81},"18","52","160","76",[64,921,130],{"x":922,"y":190,"style":923,"fill":116,"textAnchor":70},"98","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif",[64,925,927],{"x":922,"y":926,"style":455,"fill":93,"textAnchor":70},"104","into the project CRS",[256,929],{"x1":930,"y1":931,"x2":932,"y2":931,"stroke":93,"style":933},"178","90","200","stroke-width:2;marker-end:url(#pdalChainArrow)",[57,935],{"x":936,"y":917,"width":918,"height":919,"rx":194,"fill":115,"stroke":116,"style":81},"206",[64,938,152],{"x":939,"y":190,"style":923,"fill":116,"textAnchor":70},"286",[64,941,942],{"x":939,"y":926,"style":455,"fill":93,"textAnchor":70},"to the study boundary",[256,944],{"x1":945,"y1":931,"x2":139,"y2":931,"stroke":93,"style":933},"366",[57,947],{"x":948,"y":917,"width":918,"height":919,"rx":194,"fill":115,"stroke":116,"style":81},"394",[64,950,155],{"x":144,"y":190,"style":923,"fill":116,"textAnchor":70},[64,952,953],{"x":144,"y":926,"style":455,"fill":93,"textAnchor":70},"0.4 m minimum spacing",[256,955],{"x1":956,"y1":931,"x2":957,"y2":931,"stroke":93,"style":933},"554","576",[57,959],{"x":960,"y":917,"width":918,"height":919,"rx":194,"fill":166,"stroke":167,"style":81},"582",[64,962,175],{"x":963,"y":190,"style":923,"fill":167,"textAnchor":70},"662",[64,965,966],{"x":963,"y":926,"style":455,"fill":93,"textAnchor":70},"\u002Fdata\u002Foutput\u002Fdtm.tif",[57,968],{"x":916,"y":969,"width":970,"height":917,"rx":194,"fill":971,"stroke":107,"style":196},"158","536","#efeadd",[64,973,975],{"x":939,"y":974,"style":204,"fill":93,"textAnchor":70},"190","TEMPORARY_OUTPUT — three files in the session temp folder, gone at exit",[57,977],{"x":960,"y":969,"width":918,"height":917,"rx":194,"fill":166,"stroke":167,"style":196},[64,979,980],{"x":963,"y":974,"style":204,"fill":93,"textAnchor":70},"the only artefact",[57,982],{"x":983,"y":984,"width":985,"height":986,"rx":194,"fill":140,"stroke":141,"style":196},"120","234","520","42",[64,988,989],{"x":66,"y":191,"style":204,"fill":93,"textAnchor":70},"thin before exporting, not after — the grid is what costs, and it costs per point",[245,991,993],{"className":247,"code":992,"language":249,"meta":250,"style":250},"reprojected = processing.run(\"pdal:reproject\", {\n    \"INPUT\": \"\u002Fdata\u002Findexed\u002Fsurvey.vpc\",\n    \"CRS\": \"EPSG:27700\",\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n}, feedback=feedback)[\"OUTPUT\"]\n\nclipped = processing.run(\"pdal:clip\", {\n    \"INPUT\": reprojected,\n    \"OVERLAY\": \"\u002Fdata\u002Fvector\u002Fstudy_area.gpkg\",\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n}, feedback=feedback)[\"OUTPUT\"]\n\nprocessing.run(\"pdal:exportraster\", {\n    \"INPUT\": clipped,\n    \"RESOLUTION\": 1.0,\n    \"FILTER_EXPRESSION\": \"Classification == 2\",\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fdtm.tif\",\n}, feedback=feedback)\n",[18,994,995,1009,1019,1031,1042,1058,1062,1076,1083,1095,1105,1119,1123,1132,1139,1149,1159,1169],{"__ignoreMap":250},[254,996,997,1000,1002,1004,1007],{"class":256,"line":257},[254,998,999],{"class":264},"reprojected ",[254,1001,287],{"class":260},[254,1003,719],{"class":264},[254,1005,1006],{"class":317},"\"pdal:reproject\"",[254,1008,725],{"class":264},[254,1010,1011,1013,1015,1017],{"class":256,"line":274},[254,1012,731],{"class":317},[254,1014,734],{"class":264},[254,1016,737],{"class":317},[254,1018,740],{"class":264},[254,1020,1021,1024,1026,1029],{"class":256,"line":281},[254,1022,1023],{"class":317},"    \"CRS\"",[254,1025,734],{"class":264},[254,1027,1028],{"class":317},"\"EPSG:27700\"",[254,1030,740],{"class":264},[254,1032,1033,1035,1037,1040],{"class":256,"line":293},[254,1034,785],{"class":317},[254,1036,734],{"class":264},[254,1038,1039],{"class":317},"\"TEMPORARY_OUTPUT\"",[254,1041,740],{"class":264},[254,1043,1044,1046,1048,1050,1053,1055],{"class":256,"line":308},[254,1045,798],{"class":264},[254,1047,801],{"class":625},[254,1049,287],{"class":260},[254,1051,1052],{"class":264},"feedback)[",[254,1054,833],{"class":317},[254,1056,1057],{"class":264},"]\n",[254,1059,1060],{"class":256,"line":324},[254,1061,278],{"emptyLinePlaceholder":277},[254,1063,1064,1067,1069,1071,1074],{"class":256,"line":597},[254,1065,1066],{"class":264},"clipped ",[254,1068,287],{"class":260},[254,1070,719],{"class":264},[254,1072,1073],{"class":317},"\"pdal:clip\"",[254,1075,725],{"class":264},[254,1077,1078,1080],{"class":256,"line":646},[254,1079,731],{"class":317},[254,1081,1082],{"class":264},": reprojected,\n",[254,1084,1085,1088,1090,1093],{"class":256,"line":651},[254,1086,1087],{"class":317},"    \"OVERLAY\"",[254,1089,734],{"class":264},[254,1091,1092],{"class":317},"\"\u002Fdata\u002Fvector\u002Fstudy_area.gpkg\"",[254,1094,740],{"class":264},[254,1096,1097,1099,1101,1103],{"class":256,"line":662},[254,1098,785],{"class":317},[254,1100,734],{"class":264},[254,1102,1039],{"class":317},[254,1104,740],{"class":264},[254,1106,1107,1109,1111,1113,1115,1117],{"class":256,"line":685},[254,1108,798],{"class":264},[254,1110,801],{"class":625},[254,1112,287],{"class":260},[254,1114,1052],{"class":264},[254,1116,833],{"class":317},[254,1118,1057],{"class":264},[254,1120,1121],{"class":256,"line":690},[254,1122,278],{"emptyLinePlaceholder":277},[254,1124,1125,1128,1130],{"class":256,"line":695},[254,1126,1127],{"class":264},"processing.run(",[254,1129,722],{"class":317},[254,1131,725],{"class":264},[254,1133,1134,1136],{"class":256,"line":706},[254,1135,731],{"class":317},[254,1137,1138],{"class":264},": clipped,\n",[254,1140,1141,1143,1145,1147],{"class":256,"line":711},[254,1142,746],{"class":317},[254,1144,734],{"class":264},[254,1146,751],{"class":327},[254,1148,740],{"class":264},[254,1150,1151,1153,1155,1157],{"class":256,"line":728},[254,1152,772],{"class":317},[254,1154,734],{"class":264},[254,1156,777],{"class":317},[254,1158,740],{"class":264},[254,1160,1161,1163,1165,1167],{"class":256,"line":743},[254,1162,785],{"class":317},[254,1164,734],{"class":264},[254,1166,790],{"class":317},[254,1168,740],{"class":264},[254,1170,1171,1173,1175,1177],{"class":256,"line":756},[254,1172,798],{"class":264},[254,1174,801],{"class":625},[254,1176,287],{"class":260},[254,1178,806],{"class":264},[14,1180,1181,1183,1184,1187,1188,1190,1191,229],{},[218,1182,367],{}," Each ",[18,1185,1186],{},"run()"," returns a dictionary whose ",[18,1189,513],{}," is the path Processing chose, and passing that straight into the next call is all \"chaining\" means here — there is no pipeline object to build. Order matters for cost rather than correctness: clipping early throws away the points you were never going to use, so the reprojection and the grid both do less work. The same structure, and the same reasoning about what to do first, appears in ",[30,1192,1194],{"href":1193},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fchain-buffer-and-clip-pyqgis\u002F","chaining a buffer and a clip",[207,1196,1198],{"id":1197},"failing-usefully-across-many-tiles","Failing usefully across many tiles",[14,1200,1201],{},"A survey-wide job is a loop, and the interesting design question is what happens when tile forty-one is corrupt. The two useless answers are stopping the whole run and swallowing the error silently; the useful one is recording the failure and carrying on, so a four-hour job produces thirty-nine good outputs and a list of two to look at.",[245,1203,1205],{"className":247,"code":1204,"language":249,"meta":250,"style":250},"from qgis.core import QgsProcessingException\n\nfailures = []\n\nfor tile in tiles:\n    target = os.path.join(out_dir, os.path.basename(tile).replace(\".copc.laz\", \".tif\"))\n    if os.path.exists(target):\n        continue\n    try:\n        processing.run(\"pdal:exportraster\", {\n            \"INPUT\": tile,\n            \"RESOLUTION\": 1.0,\n            \"FILTER_EXPRESSION\": \"Classification == 2\",\n            \"OUTPUT\": target,\n        }, feedback=feedback)\n    except QgsProcessingException as error:\n        failures.append((tile, str(error)))\n\nfor tile, message in failures:\n    print(\"FAILED\", os.path.basename(tile), \"-\", message.splitlines()[0])\n",[18,1206,1207,1218,1222,1232,1236,1248,1269,1276,1281,1289,1298,1306,1317,1328,1336,1347,1361,1372,1376,1388],{"__ignoreMap":250},[254,1208,1209,1211,1213,1215],{"class":256,"line":257},[254,1210,261],{"class":260},[254,1212,265],{"class":264},[254,1214,268],{"class":260},[254,1216,1217],{"class":264}," QgsProcessingException\n",[254,1219,1220],{"class":256,"line":274},[254,1221,278],{"emptyLinePlaceholder":277},[254,1223,1224,1227,1229],{"class":256,"line":281},[254,1225,1226],{"class":264},"failures ",[254,1228,287],{"class":260},[254,1230,1231],{"class":264}," []\n",[254,1233,1234],{"class":256,"line":293},[254,1235,278],{"emptyLinePlaceholder":277},[254,1237,1238,1240,1243,1245],{"class":256,"line":308},[254,1239,296],{"class":260},[254,1241,1242],{"class":264}," tile ",[254,1244,302],{"class":260},[254,1246,1247],{"class":264}," tiles:\n",[254,1249,1250,1253,1255,1258,1261,1263,1266],{"class":256,"line":324},[254,1251,1252],{"class":264},"    target ",[254,1254,287],{"class":260},[254,1256,1257],{"class":264}," os.path.join(out_dir, os.path.basename(tile).replace(",[254,1259,1260],{"class":317},"\".copc.laz\"",[254,1262,622],{"class":264},[254,1264,1265],{"class":317},"\".tif\"",[254,1267,1268],{"class":264},"))\n",[254,1270,1271,1273],{"class":256,"line":597},[254,1272,311],{"class":260},[254,1274,1275],{"class":264}," os.path.exists(target):\n",[254,1277,1278],{"class":256,"line":646},[254,1279,1280],{"class":260},"        continue\n",[254,1282,1283,1286],{"class":256,"line":651},[254,1284,1285],{"class":260},"    try",[254,1287,1288],{"class":264},":\n",[254,1290,1291,1294,1296],{"class":256,"line":662},[254,1292,1293],{"class":264},"        processing.run(",[254,1295,722],{"class":317},[254,1297,725],{"class":264},[254,1299,1300,1303],{"class":256,"line":685},[254,1301,1302],{"class":317},"            \"INPUT\"",[254,1304,1305],{"class":264},": tile,\n",[254,1307,1308,1311,1313,1315],{"class":256,"line":690},[254,1309,1310],{"class":317},"            \"RESOLUTION\"",[254,1312,734],{"class":264},[254,1314,751],{"class":327},[254,1316,740],{"class":264},[254,1318,1319,1322,1324,1326],{"class":256,"line":695},[254,1320,1321],{"class":317},"            \"FILTER_EXPRESSION\"",[254,1323,734],{"class":264},[254,1325,777],{"class":317},[254,1327,740],{"class":264},[254,1329,1330,1333],{"class":256,"line":706},[254,1331,1332],{"class":317},"            \"OUTPUT\"",[254,1334,1335],{"class":264},": target,\n",[254,1337,1338,1341,1343,1345],{"class":256,"line":711},[254,1339,1340],{"class":264},"        }, ",[254,1342,801],{"class":625},[254,1344,287],{"class":260},[254,1346,806],{"class":264},[254,1348,1349,1352,1355,1358],{"class":256,"line":728},[254,1350,1351],{"class":260},"    except",[254,1353,1354],{"class":264}," QgsProcessingException ",[254,1356,1357],{"class":260},"as",[254,1359,1360],{"class":264}," error:\n",[254,1362,1363,1366,1369],{"class":256,"line":743},[254,1364,1365],{"class":264},"        failures.append((tile, ",[254,1367,1368],{"class":327},"str",[254,1370,1371],{"class":264},"(error)))\n",[254,1373,1374],{"class":256,"line":756},[254,1375,278],{"emptyLinePlaceholder":277},[254,1377,1378,1380,1383,1385],{"class":256,"line":769},[254,1379,296],{"class":260},[254,1381,1382],{"class":264}," tile, message ",[254,1384,302],{"class":260},[254,1386,1387],{"class":264}," failures:\n",[254,1389,1390,1393,1395,1398,1401,1404,1407,1409],{"class":256,"line":782},[254,1391,1392],{"class":327},"    print",[254,1394,331],{"class":264},[254,1396,1397],{"class":317},"\"FAILED\"",[254,1399,1400],{"class":264},", os.path.basename(tile), ",[254,1402,1403],{"class":317},"\"-\"",[254,1405,1406],{"class":264},", message.splitlines()[",[254,1408,59],{"class":327},[254,1410,836],{"class":264},[14,1412,1413,368,1415,1418,1419,1422,1423,1426],{},[218,1414,367],{},[18,1416,1417],{},"QgsProcessingException"," is the only exception type these algorithms raise for an operational failure — a missing input, an unwritable output, a PDAL stage error — so catching it specifically leaves genuine programming errors (a typo in a parameter name raises a plain ",[18,1420,1421],{},"KeyError"," from Processing's own validation) free to crash the script, which is what you want. The ",[18,1424,1425],{},"os.path.exists"," skip at the top makes the loop restartable: re-running after a crash picks up where it stopped instead of redoing three hours of work. Taking only the first line of the message keeps the summary readable, since PDAL errors often carry a multi-line stage trace.",[14,1428,1429],{},"One caveat specific to this family: a failed run may still have created a partial output file. Where that matters, write to a temporary name and rename on success, so a half-written raster never looks like a finished one to the next run's existence check.",[207,1431,1433],{"id":1432},"reading-pdalinfo-as-data","Reading pdal:info as data",[14,1435,1436,1438],{},[18,1437,94],{}," is the diagnostic that resolves most point cloud confusion, and it returns a JSON file rather than a layer.",[245,1440,1442],{"className":247,"code":1441,"language":249,"meta":250,"style":250},"import json\n\ninfo_path = processing.run(\"pdal:info\", {\n    \"INPUT\": \"\u002Fdata\u002Findexed\u002Ftile_0345.copc.laz\",\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})[\"OUTPUT\"]\n\nwith open(info_path) as handle:\n    info = json.load(handle)\n\nprint(info.get(\"stats\", {}).get(\"statistic\", [])[:3])\n",[18,1443,1444,1451,1455,1469,1480,1490,1499,1503,1519,1529,1533],{"__ignoreMap":250},[254,1445,1446,1448],{"class":256,"line":257},[254,1447,268],{"class":260},[254,1449,1450],{"class":264}," json\n",[254,1452,1453],{"class":256,"line":274},[254,1454,278],{"emptyLinePlaceholder":277},[254,1456,1457,1460,1462,1464,1467],{"class":256,"line":281},[254,1458,1459],{"class":264},"info_path ",[254,1461,287],{"class":260},[254,1463,719],{"class":264},[254,1465,1466],{"class":317},"\"pdal:info\"",[254,1468,725],{"class":264},[254,1470,1471,1473,1475,1478],{"class":256,"line":293},[254,1472,731],{"class":317},[254,1474,734],{"class":264},[254,1476,1477],{"class":317},"\"\u002Fdata\u002Findexed\u002Ftile_0345.copc.laz\"",[254,1479,740],{"class":264},[254,1481,1482,1484,1486,1488],{"class":256,"line":308},[254,1483,785],{"class":317},[254,1485,734],{"class":264},[254,1487,1039],{"class":317},[254,1489,740],{"class":264},[254,1491,1492,1495,1497],{"class":256,"line":324},[254,1493,1494],{"class":264},"})[",[254,1496,833],{"class":317},[254,1498,1057],{"class":264},[254,1500,1501],{"class":256,"line":597},[254,1502,278],{"emptyLinePlaceholder":277},[254,1504,1505,1508,1511,1514,1516],{"class":256,"line":646},[254,1506,1507],{"class":260},"with",[254,1509,1510],{"class":327}," open",[254,1512,1513],{"class":264},"(info_path) ",[254,1515,1357],{"class":260},[254,1517,1518],{"class":264}," handle:\n",[254,1520,1521,1524,1526],{"class":256,"line":651},[254,1522,1523],{"class":264},"    info ",[254,1525,287],{"class":260},[254,1527,1528],{"class":264}," json.load(handle)\n",[254,1530,1531],{"class":256,"line":662},[254,1532,278],{"emptyLinePlaceholder":277},[254,1534,1535,1537,1540,1543,1546,1549,1552,1555],{"class":256,"line":685},[254,1536,817],{"class":327},[254,1538,1539],{"class":264},"(info.get(",[254,1541,1542],{"class":317},"\"stats\"",[254,1544,1545],{"class":264},", {}).get(",[254,1547,1548],{"class":317},"\"statistic\"",[254,1550,1551],{"class":264},", [])[:",[254,1553,1554],{"class":327},"3",[254,1556,836],{"class":264},[14,1558,1559,1561,1562,1565,1566,1569],{},[218,1560,367],{}," The exact shape of the JSON follows PDAL's own ",[18,1563,1564],{},"--info"," output and has grown fields between PDAL releases, so read it defensively with ",[18,1567,1568],{},".get()"," rather than indexing straight in. What you are usually after is the per-attribute statistics block and the SRS block; logging both at the start of a batch run turns \"the output looks wrong\" into a diff between two runs.",[207,1571,1573],{"id":1572},"qgis-version-compatibility","QGIS version compatibility",[14,1575,1576,1579,1580,1583,1584,1587],{},[18,1577,1578],{},"pdal:"," algorithms appeared in 3.32 and the identifiers and parameter names used here are unchanged through 3.44. Two differences are worth knowing: virtual point cloud inputs need 3.34 or newer, and some builds before 3.34 name the virtual-cloud builder differently, which is exactly why the registry enumeration above is the right way to check rather than assuming. If ",[18,1581,1582],{},"algorithmById(\"pdal:info\")"," returns ",[18,1585,1586],{},"None",", the build has no PDAL provider and no amount of parameter fiddling will help.",[207,1589,1591],{"id":1590},"troubleshooting","Troubleshooting",[212,1593,1594,1610,1622,1631,1637,1647],{},[215,1595,1596,1599,1600,1602,1603,1606,1607,229],{},[218,1597,1598],{},"The output raster is entirely nodata."," The filter expression matched nothing — check for ",[18,1601,287],{}," where PDAL wants ",[18,1604,1605],{},"==",", and check the classes really exist with ",[18,1608,1609],{},"statistics().classesOf(\"Classification\")",[215,1611,1612,1621],{},[218,1613,1614,1617,1618,229],{},[18,1615,1616],{},"processing.run"," raises ",[18,1619,1620],{},"QgsProcessingException: Unknown algorithm"," No PDAL provider in this build, or Processing was never initialised in a standalone script.",[215,1623,1624,1627,1628,1630],{},[218,1625,1626],{},"The job ignores the subset string I set on the layer."," Expected: these algorithms read the file, not the layer view. Put the condition in ",[18,1629,480],{}," instead.",[215,1632,1633,1636],{},[218,1634,1635],{},"A clip returns an empty cloud."," The overlay and the cloud are in different CRSs. These algorithms do not reproject the overlay for you.",[215,1638,1639,1642,1643,1646],{},[218,1640,1641],{},"Memory use climbs until the process is killed."," Lower ",[18,1644,1645],{},"TILE_SIZE"," so the algorithm works in smaller chunks, and thin before you grid.",[215,1648,1649,1652,1653,1655],{},[218,1650,1651],{},"Progress jumps from 0 to 100 with a long pause between."," Some PDAL stages report no intermediate progress. ",[18,1654,850],{}," output still tells you which stage is running.",[207,1657,1659],{"id":1658},"conclusion","Conclusion",[14,1661,1662,1663,1665],{},"Enumerate the family from the registry rather than from memory, remember that filter expressions are PDAL's language and not QGIS's, chain through ",[18,1664,531],{}," so only the final product gets a name, and always pass a feedback object on jobs that will take more than a few seconds. With those four habits the PDAL algorithms behave like every other part of Processing, which is the whole point of them being wrapped this way.",[207,1667,1669],{"id":1668},"frequently-asked-questions","Frequently Asked Questions",[14,1671,1672,1675,1676,1678,1679,1682],{},[218,1673,1674],{},"Can I run a raw PDAL pipeline JSON from PyQGIS?","\nNot through the Processing provider — it builds its own pipelines. For a stage the wrapped algorithms do not expose, run the ",[18,1677,24],{}," executable with ",[18,1680,1681],{},"subprocess",", accepting that you lose progress and cancellation. Keep that on the edge of your code rather than in the middle of it.",[14,1684,1685,1694],{},[218,1686,1687,1688,1690,1691,1693],{},"Why is ",[18,1689,178],{}," slower than ",[18,1692,175],{},"?","\nIt triangulates the points and interpolates across the facets rather than binning them into cells, which is far more work and produces a continuous surface with no holes. Use it when ground returns are sparse and the binned output is full of gaps.",[14,1696,1697,1700,1701,229],{},[218,1698,1699],{},"Do these algorithms run in the Model Designer and the batch dialog?","\nYes — they are ordinary Processing algorithms, so they appear in models and batch runs, and a model containing them can be executed from Python exactly as described in ",[30,1702,1704],{"href":1703},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-graphical-model-from-python-pyqgis\u002F","running a graphical model from Python",[14,1706,1707,1710,1711,1714,1715,229],{},[218,1708,1709],{},"How do I run these on a machine with no display?","\nThe same way as any other algorithm, either through a headless QGIS application or ",[18,1712,1713],{},"qgis_process",". Nothing in the PDAL family needs a canvas — see ",[30,1716,1718],{"href":1717},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fuse-qgis-process-command-line-runner\u002F","using the qgis_process command line runner",[207,1720,1722],{"id":1721},"related","Related",[212,1724,1725,1730,1735,1741,1747],{},[215,1726,1727,1729],{},[30,1728,33],{"href":32}," — the guide this recipe belongs to",[215,1731,1732],{},[30,1733,1734],{"href":235},"Load a Point Cloud Layer in PyQGIS",[215,1736,1737],{},[30,1738,1740],{"href":1739},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Ffilter-and-classify-point-cloud-pyqgis\u002F","Filter and Classify a Point Cloud in PyQGIS",[215,1742,1743],{},[30,1744,1746],{"href":1745},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-processing-algorithm-from-script\u002F","Run a Processing Algorithm from a Script",[215,1748,1749],{},[30,1750,1752],{"href":1751},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fhandle-processing-feedback-and-errors-pyqgis\u002F","Handle Processing Feedback and Errors in PyQGIS",[1754,1755,1756],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":250,"searchDepth":274,"depth":274,"links":1758},[1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770],{"id":209,"depth":274,"text":210},{"id":239,"depth":274,"text":240},{"id":383,"depth":274,"text":384},{"id":535,"depth":274,"text":536},{"id":866,"depth":274,"text":867},{"id":1197,"depth":274,"text":1198},{"id":1432,"depth":274,"text":1433},{"id":1572,"depth":274,"text":1573},{"id":1590,"depth":274,"text":1591},{"id":1658,"depth":274,"text":1659},{"id":1668,"depth":274,"text":1669},{"id":1721,"depth":274,"text":1722},"[object Object]","md",{},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Frun-pdal-algorithms-from-pyqgis",{"title":5,"description":1776},{"Drive the pdal":1777,"slug":1778,"type":1779,"breadcrumb":1780,"datePublished":1781,"dateModified":1781},"Processing algorithms from Python — enumerate what your build ships, get the filter expression syntax right, chain steps through temporary outputs, and report progress.","run-pdal-algorithms-from-pyqgis","article","Run PDAL Algorithms","2026-09-04","spatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Frun-pdal-algorithms-from-pyqgis\u002Findex","4sYGlRM0o02IK_5YzBil_7lAHlJ-6cLmMKT5wnXjf8k",1788563856225]