[{"data":1,"prerenderedAt":1257},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fclip-and-tile-point-clouds-pyqgis":3},{"id":4,"title":5,"body":6,"description":1246,"extension":1247,"meta":1248,"navigation":270,"path":1253,"seo":1254,"stem":1255,"__hash__":1256},"docs\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fclip-and-tile-point-clouds-pyqgis\u002Findex.md","Clip and Tile Point Clouds in PyQGIS",{"type":7,"value":8,"toc":1234},"minimark",[9,13,17,26,208,213,239,243,344,366,377,516,530,534,537,587,603,681,684,687,691,762,781,785,788,1056,1069,1073,1093,1097,1152,1156,1159,1163,1172,1180,1186,1196,1200,1230],[10,11,5],"h1",{"id":12},"clip-and-tile-point-clouds-in-pyqgis",[14,15,16],"p",{},"A LiDAR delivery arrives tiled the way the supplier's flight lines happened to fall, which is almost never the way your work is organised. You want the survey cut to a catchment boundary, or split into pieces small enough to process in parallel, or merged into a single logical layer so a script does not have to know how many files there are. All three are one algorithm call each — the interesting part is the order you do them in and what you keep.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002F","Point Cloud & LiDAR Workflows in PyQGIS",". It covers clipping to a polygon, retiling with a buffer, building a virtual point cloud manifest, and structuring the loop so a four-hour job survives being interrupted.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,108,114,119,126,130,134,139,143,147,153,157,178,182,189,193,203],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 340","img","A survey delivered as flight-line tiles is clipped to a catchment boundary, retiled onto a regular grid, and collected into a single virtual point cloud manifest","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"From the supplier's tiling to yours",[40,41,42],"desc",{},"The delivery is a set of tiles laid out along flight lines. Clipping to the catchment boundary discards the tiles and parts of tiles outside it. Retiling lays the remaining points onto a regular grid of equal-sized pieces. A virtual point cloud manifest then presents those pieces to QGIS as a single layer.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","340","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"ctpFlowArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Clip first, retile second, manifest last",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"20","46","216","170","10","#efeadd","#59645f","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":73,"textAnchor":74},"128","72","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","as delivered",[93,94,97,102,105],"g",{"fill":95,"stroke":84,"style":96},"#e7e2d4","stroke-width:1.4",[44,98],{"x":99,"y":100,"width":101,"height":71},"40","88","176",[44,103],{"x":99,"y":104,"width":101,"height":71},"118",[44,106],{"x":99,"y":107,"width":101,"height":71},"148",[63,109],{"d":110,"fill":111,"stroke":112,"style":113},"M60 96 Q128 128 200 168","none","#0f766e","stroke-width:2.4;stroke-dasharray:5 4",[68,115,118],{"x":88,"y":116,"style":117,"fill":84,"textAnchor":74},"198","text-anchor:middle;font-size:10px;font-family:sans-serif","strips along the flight lines",[120,121],"line",{"x1":122,"y1":123,"x2":124,"y2":123,"stroke":66,"style":125},"236","130","258","stroke-width:2;marker-end:url(#ctpFlowArrow)",[44,127],{"x":128,"y":79,"width":80,"height":81,"rx":82,"fill":129,"stroke":112,"style":85},"264","#eef7f4",[68,131,133],{"x":132,"y":89,"style":90,"fill":112,"textAnchor":74},"372","clipped to the boundary",[63,135],{"d":136,"fill":137,"stroke":112,"style":138},"M300 100 Q346 84 400 96 Q446 110 442 148 Q420 186 360 182 Q306 174 296 140 Z","#e8efe6","stroke-width:2.4",[68,140,142],{"x":132,"y":141,"style":117,"fill":84,"textAnchor":74},"204","everything outside is gone",[120,144],{"x1":145,"y1":123,"x2":146,"y2":123,"stroke":66,"style":125},"480","502",[44,148],{"x":149,"y":79,"width":150,"height":81,"rx":82,"fill":151,"stroke":152,"style":85},"508","232","#eff3ff","#2563eb",[68,154,156],{"x":155,"y":89,"style":90,"fill":152,"textAnchor":74},"624","retiled on a grid",[93,158,160,165,168,171,174,176],{"fill":159,"stroke":152,"style":96},"#dbeafe",[44,161],{"x":162,"y":100,"width":163,"height":164},"532","60","42",[44,166],{"x":167,"y":100,"width":163,"height":164},"596",[44,169],{"x":170,"y":100,"width":163,"height":164},"660",[44,172],{"x":162,"y":173,"width":163,"height":164},"134",[44,175],{"x":167,"y":173,"width":163,"height":164},[44,177],{"x":170,"y":173,"width":163,"height":164},[68,179,181],{"x":155,"y":180,"style":117,"fill":84,"textAnchor":74},"200","equal pieces, parallel-friendly",[44,183],{"x":184,"y":185,"width":186,"height":164,"rx":58,"fill":187,"stroke":188,"style":85},"180","246","400","#fffdf7","#15803d",[68,190,192],{"x":70,"y":191,"style":90,"fill":188,"textAnchor":74},"272","survey.vpc — one layer over all six pieces",[44,194],{"x":195,"y":196,"width":197,"height":198,"rx":199,"fill":200,"stroke":201,"style":202},"120","300","520","30","6","#fdf2e2","#b45309","stroke-width:2",[68,204,207],{"x":70,"y":205,"style":206,"fill":66,"textAnchor":74},"320","text-anchor:middle;font-size:10.5px;font-family:sans-serif","clipping before retiling means retiling has far less to move",[209,210,212],"h2",{"id":211},"prerequisites","Prerequisites",[214,215,216,224,232],"ul",{},[217,218,219,223],"li",{},[220,221,222],"strong",{},"QGIS 3.34 LTR"," or newer with the PDAL provider (virtual point clouds need 3.34).",[217,225,226,227,231],{},"Indexed clouds — see ",[21,228,230],{"href":229},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fload-point-cloud-layer-pyqgis\u002F","loading a point cloud layer",".",[217,233,234,235,238],{},"A boundary polygon ",[220,236,237],{},"in the same CRS as the cloud",". These algorithms do not reproject the overlay for you.",[209,240,242],{"id":241},"clip-to-a-boundary","Clip to a boundary",[244,245,250],"pre",{"className":246,"code":247,"language":248,"meta":249,"style":249},"language-python shiki shiki-themes github-dark","import processing\n\nclipped = processing.run(\"pdal:clip\", {\n    \"INPUT\": \"\u002Fdata\u002Findexed\u002Fsurvey.vpc\",\n    \"OVERLAY\": \"\u002Fdata\u002Fvector\u002Fcatchment.gpkg\",\n    \"OUTPUT\": \"\u002Fdata\u002Fclip\u002Fcatchment.copc.laz\",\n})[\"OUTPUT\"]\n","python","",[251,252,253,265,272,291,306,319,332],"code",{"__ignoreMap":249},[254,255,257,261],"span",{"class":120,"line":256},1,[254,258,260],{"class":259},"snl16","import",[254,262,264],{"class":263},"s95oV"," processing\n",[254,266,268],{"class":120,"line":267},2,[254,269,271],{"emptyLinePlaceholder":270},true,"\n",[254,273,275,278,281,284,288],{"class":120,"line":274},3,[254,276,277],{"class":263},"clipped ",[254,279,280],{"class":259},"=",[254,282,283],{"class":263}," processing.run(",[254,285,287],{"class":286},"sU2Wk","\"pdal:clip\"",[254,289,290],{"class":263},", {\n",[254,292,294,297,300,303],{"class":120,"line":293},4,[254,295,296],{"class":286},"    \"INPUT\"",[254,298,299],{"class":263},": ",[254,301,302],{"class":286},"\"\u002Fdata\u002Findexed\u002Fsurvey.vpc\"",[254,304,305],{"class":263},",\n",[254,307,309,312,314,317],{"class":120,"line":308},5,[254,310,311],{"class":286},"    \"OVERLAY\"",[254,313,299],{"class":263},[254,315,316],{"class":286},"\"\u002Fdata\u002Fvector\u002Fcatchment.gpkg\"",[254,318,305],{"class":263},[254,320,322,325,327,330],{"class":120,"line":321},6,[254,323,324],{"class":286},"    \"OUTPUT\"",[254,326,299],{"class":263},[254,328,329],{"class":286},"\"\u002Fdata\u002Fclip\u002Fcatchment.copc.laz\"",[254,331,305],{"class":263},[254,333,335,338,341],{"class":120,"line":334},7,[254,336,337],{"class":263},"})[",[254,339,340],{"class":286},"\"OUTPUT\"",[254,342,343],{"class":263},"]\n",[14,345,346,349,350,353,354,357,358,361,362,365],{},[220,347,348],{},"Breakdown:"," ",[251,351,352],{},"OVERLAY"," takes a polygon layer or a path to one; every polygon in it contributes, so a multi-feature layer clips to the union rather than producing one output per feature. Passing the ",[251,355,356],{},".vpc"," as input is what makes this fast on a large survey: the manifest carries per-tile extents, so tiles that do not intersect the catchment are never opened. Writing to ",[251,359,360],{},".copc.laz"," keeps the result indexed. If the boundary and the cloud are in different projections the result is silently empty, which is the single most common failure here — check ",[251,363,364],{},"layer.crs().authid()"," on both before running.",[14,367,368,369,372,373,376],{},"Where the area of interest is a rectangle rather than a polygon, the ",[251,370,371],{},"EXTENT"," parameter on ",[251,374,375],{},"pdal:filter"," does the same job in one pass alongside a class filter, which saves a full read.",[244,378,380],{"className":246,"code":379,"language":248,"meta":249,"style":249},"from qgis.core import QgsPointCloudLayer\n\nboundary = QgsVectorLayer(\"\u002Fdata\u002Fvector\u002Fcatchment.gpkg\", \"catchment\", \"ogr\")\ncloud = QgsPointCloudLayer(\"\u002Fdata\u002Findexed\u002Fsurvey.vpc\", \"survey\", \"pdal\")\n\nif boundary.crs() != cloud.crs():\n    raise SystemExit(\n        f\"CRS mismatch: boundary {boundary.crs().authid()} vs cloud {cloud.crs().authid()}\"\n    )\n",[251,381,382,395,399,425,449,453,467,479,510],{"__ignoreMap":249},[254,383,384,387,390,392],{"class":120,"line":256},[254,385,386],{"class":259},"from",[254,388,389],{"class":263}," qgis.core ",[254,391,260],{"class":259},[254,393,394],{"class":263}," QgsPointCloudLayer\n",[254,396,397],{"class":120,"line":267},[254,398,271],{"emptyLinePlaceholder":270},[254,400,401,404,406,409,411,414,417,419,422],{"class":120,"line":274},[254,402,403],{"class":263},"boundary ",[254,405,280],{"class":259},[254,407,408],{"class":263}," QgsVectorLayer(",[254,410,316],{"class":286},[254,412,413],{"class":263},", ",[254,415,416],{"class":286},"\"catchment\"",[254,418,413],{"class":263},[254,420,421],{"class":286},"\"ogr\"",[254,423,424],{"class":263},")\n",[254,426,427,430,432,435,437,439,442,444,447],{"class":120,"line":293},[254,428,429],{"class":263},"cloud ",[254,431,280],{"class":259},[254,433,434],{"class":263}," QgsPointCloudLayer(",[254,436,302],{"class":286},[254,438,413],{"class":263},[254,440,441],{"class":286},"\"survey\"",[254,443,413],{"class":263},[254,445,446],{"class":286},"\"pdal\"",[254,448,424],{"class":263},[254,450,451],{"class":120,"line":308},[254,452,271],{"emptyLinePlaceholder":270},[254,454,455,458,461,464],{"class":120,"line":321},[254,456,457],{"class":259},"if",[254,459,460],{"class":263}," boundary.crs() ",[254,462,463],{"class":259},"!=",[254,465,466],{"class":263}," cloud.crs():\n",[254,468,469,472,476],{"class":120,"line":334},[254,470,471],{"class":259},"    raise",[254,473,475],{"class":474},"sDLfK"," SystemExit",[254,477,478],{"class":263},"(\n",[254,480,482,485,488,491,494,497,500,502,505,507],{"class":120,"line":481},8,[254,483,484],{"class":259},"        f",[254,486,487],{"class":286},"\"CRS mismatch: boundary ",[254,489,490],{"class":474},"{",[254,492,493],{"class":263},"boundary.crs().authid()",[254,495,496],{"class":474},"}",[254,498,499],{"class":286}," vs cloud ",[254,501,490],{"class":474},[254,503,504],{"class":263},"cloud.crs().authid()",[254,506,496],{"class":474},[254,508,509],{"class":286},"\"\n",[254,511,513],{"class":120,"line":512},9,[254,514,515],{"class":263},"    )\n",[14,517,518,520,521,524,525,529],{},[220,519,348],{}," A three-line guard at the top of a script converts a silent empty output into an immediate, readable failure. Comparing ",[251,522,523],{},"QgsCoordinateReferenceSystem"," objects directly is correct — the equality operator compares the definitions, not the authids, so it also catches a cloud whose CRS is a custom definition equivalent to the boundary's but labelled differently. Where they genuinely differ, reproject the ",[526,527,528],"em",{},"boundary",", which is a handful of vertices, rather than the cloud, which is a billion points.",[209,531,533],{"id":532},"retile-onto-a-regular-grid","Retile onto a regular grid",[14,535,536],{},"Flight-line tiles vary wildly in size — a strip over open country may hold ten times the points of one over water. Retiling gives you pieces of predictable cost, which is what a parallel run needs.",[244,538,540],{"className":246,"code":539,"language":248,"meta":249,"style":249},"processing.run(\"pdal:tile\", {\n    \"INPUT\": clipped,\n    \"LENGTH\": 500,\n    \"OUTPUT\": \"\u002Fdata\u002Ftiles\",\n})\n",[251,541,542,552,559,571,582],{"__ignoreMap":249},[254,543,544,547,550],{"class":120,"line":256},[254,545,546],{"class":263},"processing.run(",[254,548,549],{"class":286},"\"pdal:tile\"",[254,551,290],{"class":263},[254,553,554,556],{"class":120,"line":267},[254,555,296],{"class":286},[254,557,558],{"class":263},": clipped,\n",[254,560,561,564,566,569],{"class":120,"line":274},[254,562,563],{"class":286},"    \"LENGTH\"",[254,565,299],{"class":263},[254,567,568],{"class":474},"500",[254,570,305],{"class":263},[254,572,573,575,577,580],{"class":120,"line":293},[254,574,324],{"class":286},[254,576,299],{"class":263},[254,578,579],{"class":286},"\"\u002Fdata\u002Ftiles\"",[254,581,305],{"class":263},[254,583,584],{"class":120,"line":308},[254,585,586],{"class":263},"})\n",[14,588,589,349,591,594,595,598,599,602],{},[220,590,348],{},[251,592,593],{},"LENGTH"," is the tile edge in map units, so 500 gives 500 m squares. The ",[251,596,597],{},"OUTPUT"," here is a ",[526,600,601],{},"directory",", not a file, which is unusual among Processing algorithms and easy to miss — pointing it at a filename produces a confusing error. Choosing the length is a memory decision: aim for tiles that hold a few tens of millions of points, which on a typical 4 pts\u002Fm² survey means 500 m to 1 km squares. Smaller tiles mean more files and more edge effects; larger ones mean a single worker holds more in memory.",[14,604,605],{},[29,606,609,612,615,618,621,626,631,636,638,642,646,649,653,658,663,666,669,673,677],{"viewBox":607,"role":32,"ariaLabel":608,"xmlns":34},"0 0 760 306","Why tiled processing needs a buffer: a neighbourhood operation at a tile edge sees only half its neighbourhood unless the tile is read with an overlap that is trimmed afterwards",[36,610,611],{},"Tile edges need an overlap",[40,613,614],{},"A neighbourhood operation applied tile by tile sees a truncated neighbourhood at each edge, producing a visible seam. Reading each tile with a buffer of surrounding points, computing over the buffered area and then trimming the result back to the tile boundary removes the seam entirely.",[44,616],{"x":46,"y":46,"width":47,"height":617,"fill":49},"306",[68,619,620],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Anything that looks at neighbours needs points beyond the edge",[44,622],{"x":99,"y":623,"width":196,"height":624,"rx":82,"fill":187,"stroke":625,"style":85},"48","196","#b91c1c",[68,627,630],{"x":628,"y":629,"style":90,"fill":625,"textAnchor":74},"190","74","no buffer",[44,632],{"x":633,"y":634,"width":635,"height":635,"fill":137,"stroke":188,"style":202},"70","94","112",[44,637],{"x":116,"y":634,"width":635,"height":635,"fill":137,"stroke":188,"style":202},[120,639],{"x1":628,"y1":634,"x2":628,"y2":640,"stroke":625,"style":641},"206","stroke-width:3",[68,643,645],{"x":628,"y":644,"style":117,"fill":625,"textAnchor":74},"228","a seam along every shared edge",[44,647],{"x":648,"y":623,"width":196,"height":624,"rx":82,"fill":187,"stroke":188,"style":85},"420",[68,650,652],{"x":651,"y":629,"style":90,"fill":188,"textAnchor":74},"570","read buffered, write trimmed",[44,654],{"x":655,"y":656,"width":173,"height":88,"fill":83,"stroke":84,"style":657},"440","86","stroke-width:1.6;stroke-dasharray:5 4",[44,659],{"x":660,"y":661,"width":634,"height":662,"fill":137,"stroke":188,"style":202},"464","102","96",[44,664],{"x":665,"y":656,"width":173,"height":88,"fill":83,"stroke":84,"style":657},"582",[44,667],{"x":668,"y":661,"width":634,"height":662,"fill":137,"stroke":188,"style":202},"606",[68,670,672],{"x":651,"y":671,"style":117,"fill":66,"textAnchor":74},"234","dashed: what is read — solid: what is kept",[44,674],{"x":195,"y":675,"width":197,"height":676,"rx":199,"fill":200,"stroke":201,"style":202},"262","34",[68,678,680],{"x":70,"y":679,"style":206,"fill":66,"textAnchor":74},"284","a buffer wider than the largest neighbourhood radius you will use",[14,682,683],{},"Simple per-point operations — a class filter, a format conversion — need no buffer. Anything that looks at neighbouring points does: gridding with an interpolated statistic, a TIN export, a density surface. The practical approach is to grid each tile with a buffered read and trim the resulting raster back to the tile boundary before mosaicking, exactly as with any windowed raster operation.",[14,685,686],{},"There is one more reason to buffer that has nothing to do with seams. Several PDAL stages estimate a local property — a plane fit, a nearest-neighbour distance, a local density — and their estimate degrades near a boundary even when the output raster is later trimmed, because the estimate itself was computed from a truncated neighbourhood. Trimming hides the symptom in the middle of the tile and leaves a band of subtly wrong values just inside the trim line. The fix is the same but the buffer has to be generous: at least twice the radius the stage uses, not just enough to cover the output cells.",[209,688,690],{"id":689},"build-a-manifest-over-the-result","Build a manifest over the result",[244,692,694],{"className":246,"code":693,"language":248,"meta":249,"style":249},"import glob\n\npieces = sorted(glob.glob(\"\u002Fdata\u002Ftiles\u002F*.copc.laz\"))\n\nprocessing.run(\"pdal:virtualpointcloud\", {\n    \"LAYERS\": pieces,\n    \"OUTPUT\": \"\u002Fdata\u002Ftiles\u002Fcatchment.vpc\",\n})\n",[251,695,696,703,707,726,730,739,747,758],{"__ignoreMap":249},[254,697,698,700],{"class":120,"line":256},[254,699,260],{"class":259},[254,701,702],{"class":263}," glob\n",[254,704,705],{"class":120,"line":267},[254,706,271],{"emptyLinePlaceholder":270},[254,708,709,712,714,717,720,723],{"class":120,"line":274},[254,710,711],{"class":263},"pieces ",[254,713,280],{"class":259},[254,715,716],{"class":474}," sorted",[254,718,719],{"class":263},"(glob.glob(",[254,721,722],{"class":286},"\"\u002Fdata\u002Ftiles\u002F*.copc.laz\"",[254,724,725],{"class":263},"))\n",[254,727,728],{"class":120,"line":293},[254,729,271],{"emptyLinePlaceholder":270},[254,731,732,734,737],{"class":120,"line":308},[254,733,546],{"class":263},[254,735,736],{"class":286},"\"pdal:virtualpointcloud\"",[254,738,290],{"class":263},[254,740,741,744],{"class":120,"line":321},[254,742,743],{"class":286},"    \"LAYERS\"",[254,745,746],{"class":263},": pieces,\n",[254,748,749,751,753,756],{"class":120,"line":334},[254,750,324],{"class":286},[254,752,299],{"class":263},[254,754,755],{"class":286},"\"\u002Fdata\u002Ftiles\u002Fcatchment.vpc\"",[254,757,305],{"class":263},[254,759,760],{"class":120,"line":481},[254,761,586],{"class":263},[14,763,764,349,766,769,770,773,774,776,777,780],{},[220,765,348],{},[251,767,768],{},"LAYERS"," takes the list of member paths, and the algorithm reads each header to record extents and counts. The manifest is a few kilobytes regardless of how much data it describes, so rebuilding it is cheap and there is no reason to keep a stale one. Open the result with the ",[251,771,772],{},"pdal"," provider — a ",[251,775,356],{}," is not itself an octree, so ",[251,778,779],{},"copc"," will refuse it. From that point on, every recipe on this site that takes a cloud takes the manifest instead, and reads only the pieces it needs.",[209,782,784],{"id":783},"keeping-a-long-job-restartable","Keeping a long job restartable",[14,786,787],{},"A survey-wide clip and retile is measured in hours, and hours of work should not be lost to a full disk at the ninetieth tile.",[244,789,791],{"className":246,"code":790,"language":248,"meta":249,"style":249},"import os\n\nlog_path = \"\u002Fdata\u002Ftiles\u002Fdone.txt\"\ndone = set()\nif os.path.exists(log_path):\n    done = set(open(log_path).read().split())\n\nfor tile in sorted(glob.glob(\"\u002Fdata\u002Findexed\u002F*.copc.laz\")):\n    name = os.path.basename(tile)\n    if name in done:\n        continue\n    try:\n        processing.run(\"pdal:clip\", {\n            \"INPUT\": tile,\n            \"OVERLAY\": \"\u002Fdata\u002Fvector\u002Fcatchment.gpkg\",\n            \"OUTPUT\": f\"\u002Fdata\u002Fclip\u002F{name}\",\n        })\n    except Exception as error:\n        print(\"failed\", name, error)\n        continue\n    with open(log_path, \"a\") as handle:\n        handle.write(name + \"\\n\")\n",[251,792,793,800,804,814,827,834,852,856,877,887,901,907,916,926,935,947,973,979,994,1008,1013,1037],{"__ignoreMap":249},[254,794,795,797],{"class":120,"line":256},[254,796,260],{"class":259},[254,798,799],{"class":263}," os\n",[254,801,802],{"class":120,"line":267},[254,803,271],{"emptyLinePlaceholder":270},[254,805,806,809,811],{"class":120,"line":274},[254,807,808],{"class":263},"log_path ",[254,810,280],{"class":259},[254,812,813],{"class":286}," \"\u002Fdata\u002Ftiles\u002Fdone.txt\"\n",[254,815,816,819,821,824],{"class":120,"line":293},[254,817,818],{"class":263},"done ",[254,820,280],{"class":259},[254,822,823],{"class":474}," set",[254,825,826],{"class":263},"()\n",[254,828,829,831],{"class":120,"line":308},[254,830,457],{"class":259},[254,832,833],{"class":263}," os.path.exists(log_path):\n",[254,835,836,839,841,843,846,849],{"class":120,"line":321},[254,837,838],{"class":263},"    done ",[254,840,280],{"class":259},[254,842,823],{"class":474},[254,844,845],{"class":263},"(",[254,847,848],{"class":474},"open",[254,850,851],{"class":263},"(log_path).read().split())\n",[254,853,854],{"class":120,"line":334},[254,855,271],{"emptyLinePlaceholder":270},[254,857,858,861,864,867,869,871,874],{"class":120,"line":481},[254,859,860],{"class":259},"for",[254,862,863],{"class":263}," tile ",[254,865,866],{"class":259},"in",[254,868,716],{"class":474},[254,870,719],{"class":263},[254,872,873],{"class":286},"\"\u002Fdata\u002Findexed\u002F*.copc.laz\"",[254,875,876],{"class":263},")):\n",[254,878,879,882,884],{"class":120,"line":512},[254,880,881],{"class":263},"    name ",[254,883,280],{"class":259},[254,885,886],{"class":263}," os.path.basename(tile)\n",[254,888,890,893,896,898],{"class":120,"line":889},10,[254,891,892],{"class":259},"    if",[254,894,895],{"class":263}," name ",[254,897,866],{"class":259},[254,899,900],{"class":263}," done:\n",[254,902,904],{"class":120,"line":903},11,[254,905,906],{"class":259},"        continue\n",[254,908,910,913],{"class":120,"line":909},12,[254,911,912],{"class":259},"    try",[254,914,915],{"class":263},":\n",[254,917,919,922,924],{"class":120,"line":918},13,[254,920,921],{"class":263},"        processing.run(",[254,923,287],{"class":286},[254,925,290],{"class":263},[254,927,929,932],{"class":120,"line":928},14,[254,930,931],{"class":286},"            \"INPUT\"",[254,933,934],{"class":263},": tile,\n",[254,936,938,941,943,945],{"class":120,"line":937},15,[254,939,940],{"class":286},"            \"OVERLAY\"",[254,942,299],{"class":263},[254,944,316],{"class":286},[254,946,305],{"class":263},[254,948,950,953,955,958,961,963,966,968,971],{"class":120,"line":949},16,[254,951,952],{"class":286},"            \"OUTPUT\"",[254,954,299],{"class":263},[254,956,957],{"class":259},"f",[254,959,960],{"class":286},"\"\u002Fdata\u002Fclip\u002F",[254,962,490],{"class":474},[254,964,965],{"class":263},"name",[254,967,496],{"class":474},[254,969,970],{"class":286},"\"",[254,972,305],{"class":263},[254,974,976],{"class":120,"line":975},17,[254,977,978],{"class":263},"        })\n",[254,980,982,985,988,991],{"class":120,"line":981},18,[254,983,984],{"class":259},"    except",[254,986,987],{"class":474}," Exception",[254,989,990],{"class":259}," as",[254,992,993],{"class":263}," error:\n",[254,995,997,1000,1002,1005],{"class":120,"line":996},19,[254,998,999],{"class":474},"        print",[254,1001,845],{"class":263},[254,1003,1004],{"class":286},"\"failed\"",[254,1006,1007],{"class":263},", name, error)\n",[254,1009,1011],{"class":120,"line":1010},20,[254,1012,906],{"class":259},[254,1014,1016,1019,1022,1025,1028,1031,1034],{"class":120,"line":1015},21,[254,1017,1018],{"class":259},"    with",[254,1020,1021],{"class":474}," open",[254,1023,1024],{"class":263},"(log_path, ",[254,1026,1027],{"class":286},"\"a\"",[254,1029,1030],{"class":263},") ",[254,1032,1033],{"class":259},"as",[254,1035,1036],{"class":263}," handle:\n",[254,1038,1040,1043,1046,1049,1052,1054],{"class":120,"line":1039},22,[254,1041,1042],{"class":263},"        handle.write(name ",[254,1044,1045],{"class":259},"+",[254,1047,1048],{"class":286}," \"",[254,1050,1051],{"class":474},"\\n",[254,1053,970],{"class":286},[254,1055,424],{"class":263},[14,1057,1058,1060,1061,1064,1065,231],{},[220,1059,348],{}," Appending to the log ",[526,1062,1063],{},"after"," the algorithm returns is the whole design — a tile is only marked done once its output exists, so an interrupted run leaves the in-flight tile unmarked and the next run redoes it. Opening the file per line rather than holding a handle is deliberate: an append-and-close is durable across a kill, while a buffered handle loses whatever had not been flushed. A tile that produces no output because it falls entirely outside the catchment is a legitimate result rather than a failure, so check the point count rather than the file's existence if you care about the distinction. The same restart pattern appears in ",[21,1066,1068],{"href":1067},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis\u002F","running an algorithm over a folder of files",[209,1070,1072],{"id":1071},"qgis-version-compatibility","QGIS version compatibility",[14,1074,1075,413,1078,1081,1082,1085,1086,1089,1090,1092],{},[251,1076,1077],{},"pdal:clip",[251,1079,1080],{},"pdal:tile"," and ",[251,1083,1084],{},"pdal:merge"," arrived in 3.32. Virtual point cloud support — both the ",[251,1087,1088],{},"pdal:virtualpointcloud"," builder and reading a ",[251,1091,356],{}," — needs 3.34, and some 3.32 builds expose the builder under a different identifier, so enumerate the registry rather than assuming. Behaviour is unchanged through 3.44.",[209,1094,1096],{"id":1095},"troubleshooting","Troubleshooting",[214,1098,1099,1105,1113,1128,1134,1140],{},[217,1100,1101,1104],{},[220,1102,1103],{},"The clip output is empty."," CRS mismatch between the overlay and the cloud, or the boundary genuinely does not overlap.",[217,1106,1107,1112],{},[220,1108,1109,1111],{},[251,1110,1080],{}," errors on the output."," It wants a directory, not a filename.",[217,1114,1115,1118,1119,1121,1122,1124,1125,1127],{},[220,1116,1117],{},"The manifest will not open."," Opened with the ",[251,1120,779],{}," provider instead of ",[251,1123,772],{},", or a member path moved after the manifest was built — paths inside a ",[251,1126,356],{}," are resolved as written.",[217,1129,1130,1133],{},[220,1131,1132],{},"Seams in a gridded mosaic."," Tiles were processed without a buffer for a neighbourhood operation.",[217,1135,1136,1139],{},[220,1137,1138],{},"Disk fills during retiling."," Retiling writes a full copy of everything it keeps. Clip first so there is less of it.",[217,1141,1142,349,1145,1147,1148,1151],{},[220,1143,1144],{},"Two tiles both contain the same points.",[251,1146,1080],{}," assigns each point to exactly one tile, but a ",[526,1149,1150],{},"clip"," run per delivery tile can duplicate points in the overlap zone if the delivery tiles themselves overlap. Merge and re-tile rather than concatenating clips.",[209,1153,1155],{"id":1154},"conclusion","Conclusion",[14,1157,1158],{},"Clip before you retile so the expensive step moves less data, keep tiles in the tens of millions of points, buffer any neighbourhood operation and trim afterwards, build a manifest so downstream code sees one layer, and log completions after the fact so the job can be resumed. That is the whole of multi-tile LiDAR handling, and it is the same shape as every other batch problem in QGIS.",[209,1160,1162],{"id":1161},"frequently-asked-questions","Frequently Asked Questions",[14,1164,1165,1168,1169,1171],{},[220,1166,1167],{},"Can I clip to several polygons and get one output each?","\nNot from ",[251,1170,1077],{},", which unions the overlay. Loop over the features, writing each to its own extent-filtered output, or use the algorithm's iteration support in the batch interface.",[14,1173,1174,1177,1179],{},[220,1175,1176],{},"Does merging tiles back together lose anything?",[251,1178,1084],{}," concatenates points and takes the union of the extents. It does not deduplicate, so merging overlapping inputs gives you duplicate points, which quietly doubles the weight of the overlap zone in any statistic.",[14,1181,1182,1185],{},[220,1183,1184],{},"How large should a tile be?","\nLarge enough that per-tile overheads are negligible and small enough to fit comfortably in memory — for most work, a few tens of millions of points, which is 500 m to 1 km on a typical survey.",[14,1187,1188,1191,1192,231],{},[220,1189,1190],{},"Can a virtual point cloud reference files on a remote server?","\nYes, where the members are HTTP-accessible COPC files, since the manifest just holds URIs. Expect throughput to be dominated by how well the server honours range requests, as with ",[21,1193,1195],{"href":1194},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fread-cloud-optimized-geotiff-pyqgis\u002F","reading a cloud optimized GeoTIFF",[209,1197,1199],{"id":1198},"related","Related",[214,1201,1202,1207,1213,1219,1225],{},[217,1203,1204,1206],{},[21,1205,24],{"href":23}," — the guide this recipe belongs to",[217,1208,1209],{},[21,1210,1212],{"href":1211},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Frun-pdal-algorithms-from-pyqgis\u002F","Run the PDAL Algorithms from PyQGIS",[217,1214,1215],{},[21,1216,1218],{"href":1217},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fcreate-dem-from-point-cloud-pyqgis\u002F","Create a DEM from a Point Cloud in PyQGIS",[217,1220,1221],{},[21,1222,1224],{"href":1223},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fclip-vector-layer-pyqgis\u002F","Clip a Vector Layer in PyQGIS",[217,1226,1227],{},[21,1228,1229],{"href":1067},"Run an Algorithm Over a Folder of Files in PyQGIS",[1231,1232,1233],"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 .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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":249,"searchDepth":267,"depth":267,"links":1235},[1236,1237,1238,1239,1240,1241,1242,1243,1244,1245],{"id":211,"depth":267,"text":212},{"id":241,"depth":267,"text":242},{"id":532,"depth":267,"text":533},{"id":689,"depth":267,"text":690},{"id":783,"depth":267,"text":784},{"id":1071,"depth":267,"text":1072},{"id":1095,"depth":267,"text":1096},{"id":1154,"depth":267,"text":1155},{"id":1161,"depth":267,"text":1162},{"id":1198,"depth":267,"text":1199},"Cut a LiDAR survey to a study boundary, retile it into manageable pieces, build a virtual point cloud over the result, and keep multi-tile jobs restartable.","md",{"slug":1249,"type":1250,"breadcrumb":1251,"datePublished":1252,"dateModified":1252},"clip-and-tile-point-clouds-pyqgis","article","Clip & Tile Point Clouds","2026-09-04","\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fclip-and-tile-point-clouds-pyqgis",{"title":5,"description":1246},"spatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fclip-and-tile-point-clouds-pyqgis\u002Findex","wv7hq9uE6NWgOK6EiAWCQioT7bpzDDlXlyq7yV7-uwk",1788563855605]