[{"data":1,"prerenderedAt":1501},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fload-point-cloud-layer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1490,"extension":1491,"meta":1492,"navigation":252,"path":1497,"seo":1498,"stem":1499,"__hash__":1500},"docs\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fload-point-cloud-layer-pyqgis\u002Findex.md","Load a Point Cloud Layer in PyQGIS",{"type":7,"value":8,"toc":1478},"minimark",[9,13,17,26,170,175,209,213,219,347,377,380,531,548,552,559,654,657,811,824,828,831,884,887,945,966,970,973,1094,1215,1230,1234,1309,1315,1319,1384,1388,1398,1402,1408,1420,1429,1440,1444,1474],[10,11,5],"h1",{"id":12},"load-a-point-cloud-layer-in-pyqgis",[14,15,16],"p",{},"Opening a point cloud from Python is one line, and the line is different for each of the three formats QGIS supports. Get the provider key wrong and you get an invalid layer with a message that does not name the real problem. Get it right on an unindexed file and the layer is valid, empty for the first few seconds, and quietly rebuilding an index that it will rebuild again the next time the script runs.",[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 choosing the provider, verifying that the layer is genuinely usable rather than merely valid, dealing with a missing CRS, and structuring a script so the indexing cost is paid once.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,90,95,98,101,109,115,120,124,129,132,135,138,143,147,150,153,161,166],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 336","img","A decision path from a file on disk to the correct point cloud provider key, showing COPC and EPT opening instantly while a raw LAS or LAZ file must be indexed first","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Which provider key does this file need?",[40,41,42],"desc",{},"A file ending in copc.laz uses the copc provider and opens from its embedded octree. An ept.json file uses the ept provider and reads an index directory beside it. A plain las or laz file uses the pdal provider, which has to build an index in a cache directory before any point can be drawn.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","336","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"lpcPickArrow","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","The extension tells you the provider key",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"286","46","188","52","#efeadd","#59645f","stroke-width:2.5",[68,86,89],{"x":70,"y":87,"style":88,"fill":73,"textAnchor":74},"78","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","a file on disk",[63,91],{"d":92,"fill":93,"stroke":66,"style":94},"M380 98 L380 122 L130 122 L130 150","none","stroke-width:2;marker-end:url(#lpcPickArrow)",[63,96],{"d":97,"fill":93,"stroke":66,"style":94},"M380 98 L380 150",[63,99],{"d":100,"fill":93,"stroke":66,"style":94},"M380 98 L380 122 L630 122 L630 150",[44,102],{"x":103,"y":104,"width":105,"height":106,"rx":58,"fill":107,"stroke":108,"style":84},"24","156","212","92","#eef7f4","#0f766e",[68,110,114],{"x":111,"y":112,"style":113,"fill":108,"textAnchor":74},"130","182","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","ends .copc.laz",[68,116,119],{"x":111,"y":117,"style":118,"fill":66,"textAnchor":74},"206","text-anchor:middle;font-size:10.5px;font-family:sans-serif","provider key \"copc\"",[68,121,123],{"x":111,"y":122,"style":118,"fill":66,"textAnchor":74},"228","octree inside the file",[44,125],{"x":126,"y":104,"width":105,"height":106,"rx":58,"fill":127,"stroke":128,"style":84},"274","#eff3ff","#2563eb",[68,130,131],{"x":70,"y":112,"style":113,"fill":128,"textAnchor":74},"named ept.json",[68,133,134],{"x":70,"y":117,"style":118,"fill":66,"textAnchor":74},"provider key \"ept\"",[68,136,137],{"x":70,"y":122,"style":118,"fill":66,"textAnchor":74},"index in a folder beside it",[44,139],{"x":140,"y":104,"width":105,"height":106,"rx":58,"fill":141,"stroke":142,"style":84},"524","#fdf2e2","#b45309",[68,144,146],{"x":145,"y":112,"style":113,"fill":142,"textAnchor":74},"630","plain .las or .laz",[68,148,149],{"x":145,"y":117,"style":118,"fill":66,"textAnchor":74},"provider key \"pdal\"",[68,151,152],{"x":145,"y":122,"style":118,"fill":66,"textAnchor":74},"no index — one gets built",[44,154],{"x":155,"y":156,"width":157,"height":81,"rx":58,"fill":158,"stroke":159,"style":160},"80","272","600","#fffdf7","#b91c1c","stroke-width:2",[68,162,165],{"x":70,"y":163,"style":164,"fill":159,"textAnchor":74},"294","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","the third branch is the one that costs you",[68,167,169],{"x":70,"y":168,"style":118,"fill":66,"textAnchor":74},"314","convert once with pdal:createcopc and every later run starts from the first branch",[171,172,174],"h2",{"id":173},"prerequisites","Prerequisites",[176,177,178,186,206],"ul",{},[179,180,181,185],"li",{},[182,183,184],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, built with PDAL support — every official installer is.",[179,187,188,189,193,194,197,198,201,202,205],{},"A LiDAR file: a ",[190,191,192],"code",{},".copc.laz",", an Entwine ",[190,195,196],{},"ept.json",", or a plain ",[190,199,200],{},".las","\u002F",[190,203,204],{},".laz",".",[179,207,208],{},"Write access to somewhere you can keep converted copies, if the delivery is unindexed.",[171,210,212],{"id":211},"open-the-layer-with-the-right-provider","Open the layer with the right provider",[14,214,215,218],{},[190,216,217],{},"QgsPointCloudLayer"," takes the same three arguments as every other layer class, and the third one carries all the weight.",[220,221,226],"pre",{"className":222,"code":223,"language":224,"meta":225,"style":225},"language-python shiki shiki-themes github-dark","from qgis.core import QgsPointCloudLayer, QgsProject\n\ncloud = QgsPointCloudLayer(\n    \"\u002Fdata\u002Flidar\u002Ftile_0345.copc.laz\",\n    \"tile_0345\",\n    \"copc\",\n)\n\nif not cloud.isValid():\n    raise SystemExit(cloud.error().summary() or \"layer failed to load\")\n\nQgsProject.instance().addMapLayer(cloud)\n","python","",[190,227,228,247,254,266,276,284,292,298,303,315,336,341],{"__ignoreMap":225},[229,230,233,237,241,244],"span",{"class":231,"line":232},"line",1,[229,234,236],{"class":235},"snl16","from",[229,238,240],{"class":239},"s95oV"," qgis.core ",[229,242,243],{"class":235},"import",[229,245,246],{"class":239}," QgsPointCloudLayer, QgsProject\n",[229,248,250],{"class":231,"line":249},2,[229,251,253],{"emptyLinePlaceholder":252},true,"\n",[229,255,257,260,263],{"class":231,"line":256},3,[229,258,259],{"class":239},"cloud ",[229,261,262],{"class":235},"=",[229,264,265],{"class":239}," QgsPointCloudLayer(\n",[229,267,269,273],{"class":231,"line":268},4,[229,270,272],{"class":271},"sU2Wk","    \"\u002Fdata\u002Flidar\u002Ftile_0345.copc.laz\"",[229,274,275],{"class":239},",\n",[229,277,279,282],{"class":231,"line":278},5,[229,280,281],{"class":271},"    \"tile_0345\"",[229,283,275],{"class":239},[229,285,287,290],{"class":231,"line":286},6,[229,288,289],{"class":271},"    \"copc\"",[229,291,275],{"class":239},[229,293,295],{"class":231,"line":294},7,[229,296,297],{"class":239},")\n",[229,299,301],{"class":231,"line":300},8,[229,302,253],{"emptyLinePlaceholder":252},[229,304,306,309,312],{"class":231,"line":305},9,[229,307,308],{"class":235},"if",[229,310,311],{"class":235}," not",[229,313,314],{"class":239}," cloud.isValid():\n",[229,316,318,321,325,328,331,334],{"class":231,"line":317},10,[229,319,320],{"class":235},"    raise",[229,322,324],{"class":323},"sDLfK"," SystemExit",[229,326,327],{"class":239},"(cloud.error().summary() ",[229,329,330],{"class":235},"or",[229,332,333],{"class":271}," \"layer failed to load\"",[229,335,297],{"class":239},[229,337,339],{"class":231,"line":338},11,[229,340,253],{"emptyLinePlaceholder":252},[229,342,344],{"class":231,"line":343},12,[229,345,346],{"class":239},"QgsProject.instance().addMapLayer(cloud)\n",[14,348,349,352,353,356,357,360,361,364,365,368,369,372,373,376],{},[182,350,351],{},"Breakdown:"," Unlike ",[190,354,355],{},"QgsVectorLayer",", where ",[190,358,359],{},"\"ogr\""," covers almost everything, there is no general-purpose point cloud provider — ",[190,362,363],{},"copc",", ",[190,366,367],{},"ept"," and ",[190,370,371],{},"pdal"," are three separate readers with three separate URI conventions. ",[190,374,375],{},"error().summary()"," is worth printing rather than a generic message, because the two common failures (\"Cannot open file\" and \"Unsupported provider\") point at completely different fixes. Adding to the project is optional: an algorithm will happily take a layer that was never added, and a headless script normally should not add it.",[14,378,379],{},"Deriving the key from the path keeps a batch script honest:",[220,381,383],{"className":222,"code":382,"language":224,"meta":225,"style":225},"import os\n\ndef provider_for(path):\n    lower = path.lower()\n    if lower.endswith(\".copc.laz\"):\n        return \"copc\"\n    if os.path.basename(lower) == \"ept.json\":\n        return \"ept\"\n    if lower.endswith((\".las\", \".laz\")):\n        return \"pdal\"\n    raise ValueError(f\"not a point cloud path: {path}\")\n\ncloud = QgsPointCloudLayer(path, os.path.basename(path), provider_for(path))\n",[190,384,385,392,396,408,418,432,440,456,463,481,488,517,521],{"__ignoreMap":225},[229,386,387,389],{"class":231,"line":232},[229,388,243],{"class":235},[229,390,391],{"class":239}," os\n",[229,393,394],{"class":231,"line":249},[229,395,253],{"emptyLinePlaceholder":252},[229,397,398,401,405],{"class":231,"line":256},[229,399,400],{"class":235},"def",[229,402,404],{"class":403},"svObZ"," provider_for",[229,406,407],{"class":239},"(path):\n",[229,409,410,413,415],{"class":231,"line":268},[229,411,412],{"class":239},"    lower ",[229,414,262],{"class":235},[229,416,417],{"class":239}," path.lower()\n",[229,419,420,423,426,429],{"class":231,"line":278},[229,421,422],{"class":235},"    if",[229,424,425],{"class":239}," lower.endswith(",[229,427,428],{"class":271},"\".copc.laz\"",[229,430,431],{"class":239},"):\n",[229,433,434,437],{"class":231,"line":286},[229,435,436],{"class":235},"        return",[229,438,439],{"class":271}," \"copc\"\n",[229,441,442,444,447,450,453],{"class":231,"line":294},[229,443,422],{"class":235},[229,445,446],{"class":239}," os.path.basename(lower) ",[229,448,449],{"class":235},"==",[229,451,452],{"class":271}," \"ept.json\"",[229,454,455],{"class":239},":\n",[229,457,458,460],{"class":231,"line":300},[229,459,436],{"class":235},[229,461,462],{"class":271}," \"ept\"\n",[229,464,465,467,470,473,475,478],{"class":231,"line":305},[229,466,422],{"class":235},[229,468,469],{"class":239}," lower.endswith((",[229,471,472],{"class":271},"\".las\"",[229,474,364],{"class":239},[229,476,477],{"class":271},"\".laz\"",[229,479,480],{"class":239},")):\n",[229,482,483,485],{"class":231,"line":317},[229,484,436],{"class":235},[229,486,487],{"class":271}," \"pdal\"\n",[229,489,490,492,495,498,501,504,507,509,512,515],{"class":231,"line":338},[229,491,320],{"class":235},[229,493,494],{"class":323}," ValueError",[229,496,497],{"class":239},"(",[229,499,500],{"class":235},"f",[229,502,503],{"class":271},"\"not a point cloud path: ",[229,505,506],{"class":323},"{",[229,508,63],{"class":239},[229,510,511],{"class":323},"}",[229,513,514],{"class":271},"\"",[229,516,297],{"class":239},[229,518,519],{"class":231,"line":343},[229,520,253],{"emptyLinePlaceholder":252},[229,522,524,526,528],{"class":231,"line":523},13,[229,525,259],{"class":239},[229,527,262],{"class":235},[229,529,530],{"class":239}," QgsPointCloudLayer(path, os.path.basename(path), provider_for(path))\n",[14,532,533,535,536,538,539,541,542,544,545,547],{},[182,534,351],{}," Testing ",[190,537,192],{}," before ",[190,540,204],{}," matters, because a COPC file ends in both and the plain ",[190,543,371],{}," provider will open it — treating an already-indexed file as unindexed and rebuilding an index it already has. The ",[190,546,367],{}," case keys on the filename rather than an extension, since the URI you pass is the metadata file inside the index directory, not the directory itself.",[171,549,551],{"id":550},"valid-is-not-the-same-as-ready","Valid is not the same as ready",[14,553,554,555,558],{},"A layer built over an unindexed file reports ",[190,556,557],{},"isValid()"," immediately, because the header parsed. The octree, though, is being built on a background thread, and until it finishes the layer has an extent and a point count but nothing to draw. In an interactive session you see this as a cloud that appears a few seconds after everything else. In a script it is worse: the render or the export runs against an index that does not exist yet.",[14,560,561],{},[29,562,565,568,571,574,577,582,587,593,598,601,605,611,615,620,624,627,630,633,636,640,643,645,650],{"viewBox":563,"role":32,"ariaLabel":564,"xmlns":34},"0 0 760 300","Two timelines for loading a point cloud: an indexed file becomes drawable immediately after the header read, while an unindexed file reports valid straight away but only becomes drawable after a background indexing task finishes",[36,566,567],{},"Valid immediately, drawable later",[40,569,570],{},"For an indexed cloud the header read and the index open happen together, so the layer is drawable as soon as it is valid. For an unindexed cloud the header read returns a valid layer at once, but a background indexing task runs for seconds or minutes before the first point can be drawn, and any render started in between produces an empty image.",[44,572],{"x":46,"y":46,"width":47,"height":573,"fill":49},"300",[68,575,576],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"isValid() returns True long before the cloud can be drawn",[68,578,581],{"x":103,"y":579,"style":580,"fill":108},"66","font-size:11.5px;font-weight:bold;font-family:sans-serif","indexed (COPC \u002F EPT)",[231,583],{"x1":103,"y1":584,"x2":585,"y2":584,"stroke":83,"style":586},"82","736","stroke-width:1.6",[44,588],{"x":103,"y":589,"width":590,"height":591,"rx":592,"fill":107,"stroke":108,"style":160},"90","150","40","6",[68,594,597],{"x":595,"y":596,"style":118,"fill":66,"textAnchor":74},"99","115","header read",[44,599],{"x":600,"y":589,"width":590,"height":591,"rx":592,"fill":107,"stroke":108,"style":160},"186",[68,602,604],{"x":603,"y":596,"style":118,"fill":66,"textAnchor":74},"261","octree opened",[44,606],{"x":607,"y":589,"width":608,"height":591,"rx":592,"fill":609,"stroke":610,"style":160},"348","180","#edf8e9","#15803d",[68,612,614],{"x":613,"y":596,"style":118,"fill":66,"textAnchor":74},"438","drawable and queryable",[68,616,619],{"x":617,"y":596,"style":618,"fill":83},"560","font-size:10.5px;font-family:sans-serif","total: milliseconds",[68,621,623],{"x":103,"y":622,"style":580,"fill":142},"176","unindexed (.las \u002F .laz)",[231,625],{"x1":103,"y1":626,"x2":585,"y2":626,"stroke":83,"style":586},"192",[44,628],{"x":103,"y":629,"width":590,"height":591,"rx":592,"fill":141,"stroke":142,"style":160},"200",[68,631,597],{"x":595,"y":632,"style":118,"fill":66,"textAnchor":74},"225",[44,634],{"x":600,"y":629,"width":635,"height":591,"rx":592,"fill":141,"stroke":142,"style":160},"342",[68,637,639],{"x":638,"y":632,"style":118,"fill":66,"textAnchor":74},"357","background indexing task — seconds to minutes",[44,641],{"x":642,"y":629,"width":608,"height":591,"rx":592,"fill":609,"stroke":610,"style":160},"540",[68,644,614],{"x":145,"y":632,"style":118,"fill":66,"textAnchor":74},[68,646,649],{"x":595,"y":647,"style":648,"fill":159,"textAnchor":74},"262","text-anchor:middle;font-size:10px;font-weight:bold;font-family:sans-serif","isValid() here",[68,651,653],{"x":638,"y":652,"style":118,"fill":159,"textAnchor":74},"278","a render started anywhere in this band produces an empty image",[14,655,656],{},"The reliable answer is to stop loading unindexed files in scripts at all. Convert once, then load the converted copy:",[220,658,660],{"className":222,"code":659,"language":224,"meta":225,"style":225},"import os\nimport processing\n\ndef ensure_copc(source, cache_dir):\n    target = os.path.join(\n        cache_dir,\n        os.path.splitext(os.path.basename(source))[0] + \".copc.laz\",\n    )\n    if not os.path.exists(target):\n        processing.run(\"pdal:createcopc\", {\"INPUT\": source, \"OUTPUT\": target})\n    return target\n\npath = ensure_copc(\"\u002Fdata\u002Fdelivery\u002Ftile_0345.laz\", \"\u002Fdata\u002Findexed\")\ncloud = QgsPointCloudLayer(path, \"tile_0345\", \"copc\")\n",[190,661,662,668,675,679,689,699,704,722,727,736,759,767,771,791],{"__ignoreMap":225},[229,663,664,666],{"class":231,"line":232},[229,665,243],{"class":235},[229,667,391],{"class":239},[229,669,670,672],{"class":231,"line":249},[229,671,243],{"class":235},[229,673,674],{"class":239}," processing\n",[229,676,677],{"class":231,"line":256},[229,678,253],{"emptyLinePlaceholder":252},[229,680,681,683,686],{"class":231,"line":268},[229,682,400],{"class":235},[229,684,685],{"class":403}," ensure_copc",[229,687,688],{"class":239},"(source, cache_dir):\n",[229,690,691,694,696],{"class":231,"line":278},[229,692,693],{"class":239},"    target ",[229,695,262],{"class":235},[229,697,698],{"class":239}," os.path.join(\n",[229,700,701],{"class":231,"line":286},[229,702,703],{"class":239},"        cache_dir,\n",[229,705,706,709,711,714,717,720],{"class":231,"line":294},[229,707,708],{"class":239},"        os.path.splitext(os.path.basename(source))[",[229,710,46],{"class":323},[229,712,713],{"class":239},"] ",[229,715,716],{"class":235},"+",[229,718,719],{"class":271}," \".copc.laz\"",[229,721,275],{"class":239},[229,723,724],{"class":231,"line":300},[229,725,726],{"class":239},"    )\n",[229,728,729,731,733],{"class":231,"line":305},[229,730,422],{"class":235},[229,732,311],{"class":235},[229,734,735],{"class":239}," os.path.exists(target):\n",[229,737,738,741,744,747,750,753,756],{"class":231,"line":317},[229,739,740],{"class":239},"        processing.run(",[229,742,743],{"class":271},"\"pdal:createcopc\"",[229,745,746],{"class":239},", {",[229,748,749],{"class":271},"\"INPUT\"",[229,751,752],{"class":239},": source, ",[229,754,755],{"class":271},"\"OUTPUT\"",[229,757,758],{"class":239},": target})\n",[229,760,761,764],{"class":231,"line":338},[229,762,763],{"class":235},"    return",[229,765,766],{"class":239}," target\n",[229,768,769],{"class":231,"line":343},[229,770,253],{"emptyLinePlaceholder":252},[229,772,773,776,778,781,784,786,789],{"class":231,"line":523},[229,774,775],{"class":239},"path ",[229,777,262],{"class":235},[229,779,780],{"class":239}," ensure_copc(",[229,782,783],{"class":271},"\"\u002Fdata\u002Fdelivery\u002Ftile_0345.laz\"",[229,785,364],{"class":239},[229,787,788],{"class":271},"\"\u002Fdata\u002Findexed\"",[229,790,297],{"class":239},[229,792,794,796,798,801,804,806,809],{"class":231,"line":793},14,[229,795,259],{"class":239},[229,797,262],{"class":235},[229,799,800],{"class":239}," QgsPointCloudLayer(path, ",[229,802,803],{"class":271},"\"tile_0345\"",[229,805,364],{"class":239},[229,807,808],{"class":271},"\"copc\"",[229,810,297],{"class":239},[14,812,813,815,816,819,820,823],{},[182,814,351],{}," The existence check makes the function idempotent, so a batch job that is re-run after a failure skips everything it already converted — the same pattern as any other cached derivation. ",[190,817,818],{},"pdal:createcopc"," is synchronous inside ",[190,821,822],{},"processing.run()",", which is exactly the property the background indexer lacks: when the call returns, the index is on disk and complete. Keep the cache directory outside the delivery folder so a re-delivery does not mix converted and original files.",[171,825,827],{"id":826},"fixing-a-missing-or-wrong-crs","Fixing a missing or wrong CRS",[14,829,830],{},"LAS stores its projection in a variable-length record, and plenty of producers write something QGIS cannot interpret. The symptom is a layer that loads fine and sits at coordinates that make no sense next to the rest of the project.",[220,832,834],{"className":222,"code":833,"language":224,"meta":225,"style":225},"crs = cloud.crs()\nprint(\"authid:\", crs.authid() or \"(none)\")\nprint(\"extent:\", cloud.extent().toString(1))\n",[190,835,836,846,866],{"__ignoreMap":225},[229,837,838,841,843],{"class":231,"line":232},[229,839,840],{"class":239},"crs ",[229,842,262],{"class":235},[229,844,845],{"class":239}," cloud.crs()\n",[229,847,848,851,853,856,859,861,864],{"class":231,"line":249},[229,849,850],{"class":323},"print",[229,852,497],{"class":239},[229,854,855],{"class":271},"\"authid:\"",[229,857,858],{"class":239},", crs.authid() ",[229,860,330],{"class":235},[229,862,863],{"class":271}," \"(none)\"",[229,865,297],{"class":239},[229,867,868,870,872,875,878,881],{"class":231,"line":256},[229,869,850],{"class":323},[229,871,497],{"class":239},[229,873,874],{"class":271},"\"extent:\"",[229,876,877],{"class":239},", cloud.extent().toString(",[229,879,880],{"class":323},"1",[229,882,883],{"class":239},"))\n",[14,885,886],{},"If the authid is empty, do not set the CRS on the layer object and move on — that only relabels the layer in memory and the next script to open the file has the same problem. Write it into the file:",[220,888,890],{"className":222,"code":889,"language":224,"meta":225,"style":225},"processing.run(\"pdal:assignprojection\", {\n    \"INPUT\": \"\u002Fdata\u002Findexed\u002Ftile_0345.copc.laz\",\n    \"CRS\": \"EPSG:27700\",\n    \"OUTPUT\": \"\u002Fdata\u002Findexed\u002Ftile_0345_crs.copc.laz\",\n})\n",[190,891,892,903,916,928,940],{"__ignoreMap":225},[229,893,894,897,900],{"class":231,"line":232},[229,895,896],{"class":239},"processing.run(",[229,898,899],{"class":271},"\"pdal:assignprojection\"",[229,901,902],{"class":239},", {\n",[229,904,905,908,911,914],{"class":231,"line":249},[229,906,907],{"class":271},"    \"INPUT\"",[229,909,910],{"class":239},": ",[229,912,913],{"class":271},"\"\u002Fdata\u002Findexed\u002Ftile_0345.copc.laz\"",[229,915,275],{"class":239},[229,917,918,921,923,926],{"class":231,"line":256},[229,919,920],{"class":271},"    \"CRS\"",[229,922,910],{"class":239},[229,924,925],{"class":271},"\"EPSG:27700\"",[229,927,275],{"class":239},[229,929,930,933,935,938],{"class":231,"line":268},[229,931,932],{"class":271},"    \"OUTPUT\"",[229,934,910],{"class":239},[229,936,937],{"class":271},"\"\u002Fdata\u002Findexed\u002Ftile_0345_crs.copc.laz\"",[229,939,275],{"class":239},[229,941,942],{"class":231,"line":278},[229,943,944],{"class":239},"})\n",[14,946,947,949,950,949,953,957,958,961,962,205],{},[182,948,351],{}," ",[190,951,952],{},"pdal:assignprojection",[954,955,956],"em",{},"declares"," a projection without moving any coordinates, which is what you want when the numbers are right and only the label is missing. Its sibling ",[190,959,960],{},"pdal:reproject"," actually transforms the coordinates and is what you want when the file is genuinely in the wrong system. Confusing the two produces a cloud that is wrong by the distance between two projections, and the mistake is invisible until you overlay something else — the same distinction that applies to ",[21,963,965],{"href":964},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fhandling-missing-crs-in-pyqgis\u002F","handling a missing CRS on a vector layer",[171,967,969],{"id":968},"loading-a-whole-survey-as-one-layer","Loading a whole survey as one layer",[14,971,972],{},"Adding forty tiles to a project gives you forty entries in the layer tree and forty things to iterate. A virtual point cloud collapses them into one.",[220,974,976],{"className":222,"code":975,"language":224,"meta":225,"style":225},"import glob\n\ntiles = sorted(glob.glob(\"\u002Fdata\u002Findexed\u002F*.copc.laz\"))\n\nprocessing.run(\"pdal:virtualpointcloud\", {\n    \"LAYERS\": tiles,\n    \"OUTPUT\": \"\u002Fdata\u002Findexed\u002Fsurvey.vpc\",\n})\n\nsurvey = QgsPointCloudLayer(\"\u002Fdata\u002Findexed\u002Fsurvey.vpc\", \"survey\", \"pdal\")\nprint(survey.pointCount(), \"points across\", len(tiles), \"tiles\")\n",[190,977,978,985,989,1007,1011,1020,1028,1039,1043,1047,1071],{"__ignoreMap":225},[229,979,980,982],{"class":231,"line":232},[229,981,243],{"class":235},[229,983,984],{"class":239}," glob\n",[229,986,987],{"class":231,"line":249},[229,988,253],{"emptyLinePlaceholder":252},[229,990,991,994,996,999,1002,1005],{"class":231,"line":256},[229,992,993],{"class":239},"tiles ",[229,995,262],{"class":235},[229,997,998],{"class":323}," sorted",[229,1000,1001],{"class":239},"(glob.glob(",[229,1003,1004],{"class":271},"\"\u002Fdata\u002Findexed\u002F*.copc.laz\"",[229,1006,883],{"class":239},[229,1008,1009],{"class":231,"line":268},[229,1010,253],{"emptyLinePlaceholder":252},[229,1012,1013,1015,1018],{"class":231,"line":278},[229,1014,896],{"class":239},[229,1016,1017],{"class":271},"\"pdal:virtualpointcloud\"",[229,1019,902],{"class":239},[229,1021,1022,1025],{"class":231,"line":286},[229,1023,1024],{"class":271},"    \"LAYERS\"",[229,1026,1027],{"class":239},": tiles,\n",[229,1029,1030,1032,1034,1037],{"class":231,"line":294},[229,1031,932],{"class":271},[229,1033,910],{"class":239},[229,1035,1036],{"class":271},"\"\u002Fdata\u002Findexed\u002Fsurvey.vpc\"",[229,1038,275],{"class":239},[229,1040,1041],{"class":231,"line":300},[229,1042,944],{"class":239},[229,1044,1045],{"class":231,"line":305},[229,1046,253],{"emptyLinePlaceholder":252},[229,1048,1049,1052,1054,1057,1059,1061,1064,1066,1069],{"class":231,"line":317},[229,1050,1051],{"class":239},"survey ",[229,1053,262],{"class":235},[229,1055,1056],{"class":239}," QgsPointCloudLayer(",[229,1058,1036],{"class":271},[229,1060,364],{"class":239},[229,1062,1063],{"class":271},"\"survey\"",[229,1065,364],{"class":239},[229,1067,1068],{"class":271},"\"pdal\"",[229,1070,297],{"class":239},[229,1072,1073,1075,1078,1081,1083,1086,1089,1092],{"class":231,"line":338},[229,1074,850],{"class":323},[229,1076,1077],{"class":239},"(survey.pointCount(), ",[229,1079,1080],{"class":271},"\"points across\"",[229,1082,364],{"class":239},[229,1084,1085],{"class":323},"len",[229,1087,1088],{"class":239},"(tiles), ",[229,1090,1091],{"class":271},"\"tiles\"",[229,1093,297],{"class":239},[14,1095,1096],{},[29,1097,1100,1103,1106,1109,1116,1119,1123,1128,1134,1138,1142,1146,1150,1154,1158,1163,1168,1172,1177,1181,1183,1186,1188,1190,1193,1195,1197,1203,1206,1211],{"viewBox":1098,"role":32,"ariaLabel":1099,"xmlns":34},"0 0 760 316","A virtual point cloud manifest listing tile paths and extents, so that a clip over a small area of interest opens only the two tiles it overlaps instead of all nine",[36,1101,1102],{},"What a virtual point cloud buys you",[40,1104,1105],{},"A vpc file is a small manifest holding each member tile's path, extent and point count. Because the extents are in the manifest, an operation over a small area of interest consults the manifest first and opens only the tiles that intersect it, leaving the rest of the survey untouched.",[44,1107],{"x":46,"y":46,"width":47,"height":1108,"fill":49},"316",[51,1110,1111],{},[54,1112,1114],{"id":1113,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"lpcVpcArrow",[63,1115],{"d":65,"fill":66},[68,1117,1118],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"The manifest is read; most of the tiles never are",[44,1120],{"x":103,"y":1121,"width":1122,"height":105,"rx":58,"fill":158,"stroke":108,"style":84},"48","270",[68,1124,1127],{"x":1125,"y":1126,"style":113,"fill":108,"textAnchor":74},"159","74","survey.vpc",[68,1129,1133],{"x":1130,"y":1131,"style":1132,"fill":66},"44","100","font-size:10px;font-family:sans-serif","tile_0344.copc.laz · 41 Mpts",[68,1135,1137],{"x":1130,"y":1136,"style":1132,"fill":66},"122","tile_0345.copc.laz · 39 Mpts",[68,1139,1141],{"x":1130,"y":1140,"style":1132,"fill":66},"144","tile_0346.copc.laz · 44 Mpts",[68,1143,1145],{"x":1130,"y":1144,"style":1132,"fill":83},"166","… six more members",[68,1147,1149],{"x":1130,"y":1148,"style":1132,"fill":66},"196","each entry carries an extent",[68,1151,1153],{"x":1130,"y":1152,"style":1132,"fill":66},"218","and a point count",[68,1155,1157],{"x":1130,"y":1156,"style":1132,"fill":83},"242","about 4 kB on disk",[231,1159],{"x1":163,"y1":1160,"x2":1161,"y2":1160,"stroke":66,"style":1162},"154","330","stroke-width:2;marker-end:url(#lpcVpcArrow)",[44,1164],{"x":1165,"y":1121,"width":1166,"height":105,"rx":58,"fill":1167,"stroke":83,"style":160},"340","396","#e7e2d4",[68,1169,1171],{"x":1170,"y":1126,"style":164,"fill":73,"textAnchor":74},"538","the survey on the ground",[44,1173],{"x":1174,"y":1175,"width":1176,"height":81,"fill":82,"stroke":83,"style":586},"364","88","112",[44,1178],{"x":1179,"y":1175,"width":1176,"height":81,"fill":107,"stroke":108,"style":1180},"482","stroke-width:2.4",[44,1182],{"x":157,"y":1175,"width":1176,"height":81,"fill":82,"stroke":83,"style":586},[44,1184],{"x":1174,"y":1185,"width":1176,"height":81,"fill":82,"stroke":83,"style":586},"146",[44,1187],{"x":1179,"y":1185,"width":1176,"height":81,"fill":107,"stroke":108,"style":1180},[44,1189],{"x":157,"y":1185,"width":1176,"height":81,"fill":82,"stroke":83,"style":586},[44,1191],{"x":1174,"y":1192,"width":1176,"height":591,"fill":82,"stroke":83,"style":586},"204",[44,1194],{"x":1179,"y":1192,"width":1176,"height":591,"fill":82,"stroke":83,"style":586},[44,1196],{"x":157,"y":1192,"width":1176,"height":591,"fill":82,"stroke":83,"style":586},[44,1198],{"x":1199,"y":1176,"width":1200,"height":1201,"fill":93,"stroke":142,"style":1202},"506","64","60","stroke-width:2.6;stroke-dasharray:6 4",[68,1204,1205],{"x":1170,"y":156,"style":118,"fill":142,"textAnchor":74},"dashed box: the area of interest — two tiles opened, seven skipped",[44,1207],{"x":1208,"y":78,"width":1209,"height":103,"rx":592,"fill":141,"stroke":142,"style":1210},"120","520","stroke-width:1.8",[68,1212,1214],{"x":70,"y":1213,"style":118,"fill":66,"textAnchor":74},"303","without the manifest, the same clip reads every tile to discover it is empty",[14,1216,1217,1219,1220,1223,1224,1226,1227,1229],{},[182,1218,351],{}," The ",[190,1221,1222],{},".vpc"," is a small JSON manifest holding each member's path, extent and point count, so building it is fast and reading it is faster. Note the provider key: a virtual point cloud is opened through ",[190,1225,371],{},", not ",[190,1228,363],{},", because the manifest is not itself an octree. Because the manifest carries per-tile extents, a clip or an export against this layer opens only the tiles that intersect the requested area — which is the entire reason to build it.",[171,1231,1233],{"id":1232},"qgis-version-compatibility","QGIS version compatibility",[1235,1236,1237,1250],"table",{},[1238,1239,1240],"thead",{},[1241,1242,1243,1247],"tr",{},[1244,1245,1246],"th",{},"Version",[1244,1248,1249],{},"Point cloud support",[1251,1252,1253,1264,1272,1287,1298],"tbody",{},[1241,1254,1255,1259],{},[1256,1257,1258],"td",{},"3.18",[1256,1260,1261,1263],{},[190,1262,217],{}," and the EPT provider arrive",[1241,1265,1266,1269],{},[1256,1267,1268],{},"3.26",[1256,1270,1271],{},"COPC provider; subset strings on point cloud layers",[1241,1273,1274,1277],{},[1256,1275,1276],{},"3.32",[1256,1278,1279,1280,1283,1284],{},"The ",[190,1281,1282],{},"pdal:"," Processing algorithms, including ",[190,1285,1286],{},"createcopc",[1241,1288,1289,1292],{},[1256,1290,1291],{},"3.34 LTR",[1256,1293,1294,1295,1297],{},"Virtual point clouds (",[190,1296,1222],{},"); the baseline for everything here",[1241,1299,1300,1303],{},[1256,1301,1302],{},"3.40 LTR",[1256,1304,1305,1306,1308],{},"Faster index reads; ",[190,1307,1282],{}," parameter names unchanged",[14,1310,1311,1312,1314],{},"Code written against the ",[190,1313,1282],{}," algorithms and the layer-level API above runs unchanged from 3.34 onwards. The low-level index classes did change names between 3.34 and 3.40, which is the reason this recipe does not use them.",[171,1316,1318],{"id":1317},"troubleshooting","Troubleshooting",[176,1320,1321,1345,1351,1357,1366,1378],{},[179,1322,1323,1328,1329,1331,1332,1334,1335,1337,1338,1341,1342,1344],{},[182,1324,1325,1327],{},[190,1326,557],{}," is False on a file that opens in the QGIS browser."," The provider key is wrong — most often ",[190,1330,371],{}," was passed for a ",[190,1333,192],{},", or ",[190,1336,367],{}," was passed for the index ",[954,1339,1340],{},"directory"," rather than the ",[190,1343,196],{}," inside it.",[179,1346,1347,1350],{},[182,1348,1349],{},"The layer loads but draws nothing."," Either the index is still building (see above), or the point budget is exhausted by another dense layer in the project.",[179,1352,1353,1356],{},[182,1354,1355],{},"The extent is a tiny box near zero."," The header has no CRS and no scale, or the file is a header-only stub from an interrupted transfer.",[179,1358,1359,1362,1363,1365],{},[182,1360,1361],{},"The cloud is in the sea off West Africa."," No CRS on the header. Fix it with ",[190,1364,952],{}," rather than by setting the CRS on the layer.",[179,1367,1368,1373,1374,1377],{},[182,1369,1370,1372],{},[190,1371,818],{}," fails on a file that loads fine."," The file is a LAS 1.4 variant with a point format PDAL will not write to COPC; convert with ",[190,1375,1376],{},"pdal:convertformat"," first.",[179,1379,1380,1383],{},[182,1381,1382],{},"Every run of the script takes minutes before anything happens."," Unindexed inputs. Convert once and cache, as above.",[171,1385,1387],{"id":1386},"conclusion","Conclusion",[14,1389,1390,1391,1393,1394,1397],{},"Pick the provider key from the file, treat ",[190,1392,557],{}," as the start of the check rather than the end, and never let a production script open an unindexed file. Convert deliveries to COPC once, build a virtual point cloud over the tiles, and the rest of the work in ",[21,1395,1396],{"href":23},"this guide's other recipes"," starts from a layer that is ready the instant it is constructed.",[171,1399,1401],{"id":1400},"frequently-asked-questions","Frequently Asked Questions",[14,1403,1404,1407],{},[182,1405,1406],{},"Can I load a point cloud from a URL?","\nYes, for COPC and EPT, which are both designed for range requests over HTTP. Pass the URL as the URI with the same provider key. Throughput depends entirely on the server honouring range requests; one that does not will download the whole file for every pan.",[14,1409,1410,1413,1414,1416,1417,1419],{},[182,1411,1412],{},"Does QGIS support LAS 1.4 and the newer point formats?","\nIt reads them. Some conversions are narrower than the reader — ",[190,1415,818],{}," in particular will refuse a few exotic point formats, in which case ",[190,1418,1376],{}," to a common format first.",[14,1421,1422,1425,1428],{},[182,1423,1424],{},"How do I unload a cloud and free its memory?",[190,1426,1427],{},"QgsProject.instance().removeMapLayer(cloud.id())"," if it was added, or simply let the Python reference go out of scope if it was not. The index cache on disk is separate and is not cleaned up by either.",[14,1430,1431,1434,1435,356,1437,1439],{},[182,1432,1433],{},"Is there a way to see indexing progress?","\nThe task appears in the QGIS task manager in the GUI. Headless there is no progress reporting, which is another argument for converting explicitly with ",[190,1436,818],{},[190,1438,822],{}," gives you a feedback object.",[171,1441,1443],{"id":1442},"related","Related",[176,1445,1446,1451,1457,1463,1468],{},[179,1447,1448,1450],{},[21,1449,24],{"href":23}," — the guide this recipe belongs to",[179,1452,1453],{},[21,1454,1456],{"href":1455},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Frun-pdal-algorithms-from-pyqgis\u002F","Run the PDAL Algorithms from PyQGIS",[179,1458,1459],{},[21,1460,1462],{"href":1461},"\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fclip-and-tile-point-clouds-pyqgis\u002F","Clip and Tile Point Clouds in PyQGIS",[179,1464,1465],{},[21,1466,1467],{"href":964},"Handling a Missing CRS in PyQGIS",[179,1469,1470],{},[21,1471,1473],{"href":1472},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fread-cloud-optimized-geotiff-pyqgis\u002F","Read a Cloud Optimized GeoTIFF in PyQGIS",[1475,1476,1477],"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}",{"title":225,"searchDepth":249,"depth":249,"links":1479},[1480,1481,1482,1483,1484,1485,1486,1487,1488,1489],{"id":173,"depth":249,"text":174},{"id":211,"depth":249,"text":212},{"id":550,"depth":249,"text":551},{"id":826,"depth":249,"text":827},{"id":968,"depth":249,"text":969},{"id":1232,"depth":249,"text":1233},{"id":1317,"depth":249,"text":1318},{"id":1386,"depth":249,"text":1387},{"id":1400,"depth":249,"text":1401},{"id":1442,"depth":249,"text":1443},"Open COPC, EPT and raw LAZ point clouds from Python with the right provider key, check the CRS and index before you use them, and avoid re-indexing on every run.","md",{"slug":1493,"type":1494,"breadcrumb":1495,"datePublished":1496,"dateModified":1496},"load-point-cloud-layer-pyqgis","article","Load a Point Cloud Layer","2026-09-04","\u002Fspatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fload-point-cloud-layer-pyqgis",{"title":5,"description":1490},"spatial-data-processing-automation\u002Fpoint-cloud-and-lidar-workflows\u002Fload-point-cloud-layer-pyqgis\u002Findex","jAt9nBbUapLzIbh4vz3dIJwch0GDVfn-xd5u_YGtviI",1788563856127]