[{"data":1,"prerenderedAt":1438},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fcreate-custom-crs-from-proj-string-pyqgis":3},{"id":4,"title":5,"body":6,"description":1427,"extension":1428,"meta":1429,"navigation":228,"path":1434,"seo":1435,"stem":1436,"__hash__":1437},"docs\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fcreate-custom-crs-from-proj-string-pyqgis\u002Findex.md","Create a Custom CRS from a PROJ String in PyQGIS",{"type":7,"value":8,"toc":1415},"minimark",[9,13,17,26,150,155,177,181,195,366,384,395,399,402,525,802,816,820,823,1019,1037,1108,1112,1115,1237,1255,1259,1289,1293,1337,1341,1350,1354,1360,1366,1372,1378,1382,1411],[10,11,5],"h1",{"id":12},"create-a-custom-crs-from-a-proj-string-in-pyqgis",[14,15,16],"p",{},"The EPSG registry holds thousands of coordinate reference systems, and it is still not enough. A construction site uses a local grid scaled to ground distance. A mine surveys in a transverse Mercator centred on the pit. An older municipal dataset is in a system its surveyors defined in the 1970s and nobody registered. A drone mapping project uses a low-distortion projection centred on the project area. For all of these, the data is only usable once QGIS knows the CRS — which means defining it yourself.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems in PyQGIS",". It builds CRSs from PROJ strings and WKT, validates them against known control points, registers them as user CRSs so they appear in the CRS selector, and deals with the portability problem that custom CRSs create.",[14,27,28],{},[29,30,35,39,43,50,59,69,75,83,89,94,98,102,106,112,116,119,122,127,131,137,141,144,147],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 300","img","The parts of a custom projected CRS definition: a geodetic datum and ellipsoid, a projection method with its parameters such as central meridian, latitude of origin, scale factor and false easting and northing, and the axis units","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"What a projected CRS definition contains",[40,41,42],"desc",{},"A PROJ string for a site grid is broken into three groups. The projection group: proj equals tmerc with lat_0, lon_0, k scale factor, x_0 false easting and y_0 false northing. The datum group: ellps equals GRS80 with a towgs84 shift, or a named datum. The units group: units equals m and no_defs. Together they define how latitude and longitude on the ellipsoid become easting and northing on the site plan.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","300","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Three things every projected CRS must say",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"24","48","712","46","8","#fffdf7","#59645f","stroke-width:2",[51,70,74],{"x":53,"y":71,"style":72,"fill":73,"textAnchor":57},"76","text-anchor:middle;font-size:10.5px;font-family:monospace","#2f3b35","+proj=tmerc +lat_0=51.5 +lon_0=-0.12 +k=1.00002 +x_0=5000 +y_0=10000 +ellps=GRS80 +units=m",[44,76],{"x":61,"y":77,"width":48,"height":78,"rx":79,"fill":80,"stroke":81,"style":82},"116","160","10","#eef7f4","#0f766e","stroke-width:2.5",[51,84,88],{"x":85,"y":86,"style":87,"fill":81,"textAnchor":57},"174","142","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","projection",[51,90,93],{"x":85,"y":91,"style":92,"fill":73,"textAnchor":57},"170","text-anchor:middle;font-size:10px;font-family:monospace","proj=tmerc",[51,95,97],{"x":85,"y":96,"style":92,"fill":73,"textAnchor":57},"190","lat_0, lon_0 — origin",[51,99,101],{"x":85,"y":100,"style":92,"fill":73,"textAnchor":57},"210","k — scale factor",[51,103,105],{"x":85,"y":104,"style":92,"fill":73,"textAnchor":57},"230","x_0, y_0 — false origin",[44,107],{"x":108,"y":77,"width":109,"height":78,"rx":79,"fill":110,"stroke":111,"style":82},"344","200","#eff3ff","#2563eb",[51,113,115],{"x":114,"y":86,"style":87,"fill":111,"textAnchor":57},"444","datum",[51,117,118],{"x":114,"y":91,"style":92,"fill":73,"textAnchor":57},"ellps=GRS80",[51,120,121],{"x":114,"y":96,"style":92,"fill":73,"textAnchor":57},"or datum=…",[51,123,126],{"x":114,"y":124,"style":125,"fill":67,"textAnchor":57},"222","text-anchor:middle;font-size:9.5px;font-family:sans-serif","decides how it relates",[51,128,130],{"x":114,"y":129,"style":125,"fill":67,"textAnchor":57},"238","to WGS 84",[44,132],{"x":133,"y":77,"width":134,"height":78,"rx":79,"fill":135,"stroke":136,"style":82},"564","172","#fdf2e2","#b45309",[51,138,140],{"x":139,"y":86,"style":87,"fill":136,"textAnchor":57},"650","units",[51,142,143],{"x":139,"y":91,"style":92,"fill":73,"textAnchor":57},"units=m",[51,145,146],{"x":139,"y":109,"style":125,"fill":67,"textAnchor":57},"or us-ft, ft",[51,148,149],{"x":139,"y":104,"style":125,"fill":67,"textAnchor":57},"distances follow these",[151,152,154],"h2",{"id":153},"prerequisites","Prerequisites",[156,157,158,166,169],"ul",{},[159,160,161,165],"li",{},[162,163,164],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series. All use PROJ 9, which prefers WKT2 but reads PROJ strings.",[159,167,168],{},"The CRS parameters from the surveyor or the data's documentation, and ideally at least one control point with coordinates known in both the custom CRS and a standard one.",[159,170,171,172,176],{},"Before defining anything, a check that the CRS is genuinely missing: search ",[173,174,175],"code",{},"QgsCoordinateReferenceSystem(\"EPSG:…\")"," and the CRS selector for the projection name first. Many \"custom\" systems are registered under a name nobody recognised.",[151,178,180],{"id":179},"build-the-crs-and-check-it-is-valid","Build the CRS and check it is valid",[14,182,183,186,187,190,191,194],{},[173,184,185],{},"QgsCoordinateReferenceSystem.fromProj"," parses a PROJ string; ",[173,188,189],{},"fromWkt"," parses WKT. Either returns a CRS object whose ",[173,192,193],{},"isValid()"," says whether PROJ accepted the definition.",[196,197,202],"pre",{"className":198,"code":199,"language":200,"meta":201,"style":201},"language-python shiki shiki-themes github-dark","from qgis.core import Qgis, QgsCoordinateReferenceSystem\n\nSITE_GRID = (\n    \"+proj=tmerc +lat_0=51.5 +lon_0=-0.12 +k=1.00002 \"\n    \"+x_0=5000 +y_0=10000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs\"\n)\n\nsite = QgsCoordinateReferenceSystem.fromProj(SITE_GRID)\nif not site.isValid():\n    raise ValueError(\"PROJ rejected the definition\")\n\nprint(\"description:\", site.description() or \"(unnamed)\")\nprint(\"geographic?\", site.isGeographic(), \"units:\", site.mapUnits())\nprint(site.toWkt(Qgis.CrsWktVariant.Wkt2_2019Simplified))\n","python","",[173,203,204,223,230,243,250,256,262,267,283,295,312,317,339,358],{"__ignoreMap":201},[205,206,209,213,217,220],"span",{"class":207,"line":208},"line",1,[205,210,212],{"class":211},"snl16","from",[205,214,216],{"class":215},"s95oV"," qgis.core ",[205,218,219],{"class":211},"import",[205,221,222],{"class":215}," Qgis, QgsCoordinateReferenceSystem\n",[205,224,226],{"class":207,"line":225},2,[205,227,229],{"emptyLinePlaceholder":228},true,"\n",[205,231,233,237,240],{"class":207,"line":232},3,[205,234,236],{"class":235},"sDLfK","SITE_GRID",[205,238,239],{"class":211}," =",[205,241,242],{"class":215}," (\n",[205,244,246],{"class":207,"line":245},4,[205,247,249],{"class":248},"sU2Wk","    \"+proj=tmerc +lat_0=51.5 +lon_0=-0.12 +k=1.00002 \"\n",[205,251,253],{"class":207,"line":252},5,[205,254,255],{"class":248},"    \"+x_0=5000 +y_0=10000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs\"\n",[205,257,259],{"class":207,"line":258},6,[205,260,261],{"class":215},")\n",[205,263,265],{"class":207,"line":264},7,[205,266,229],{"emptyLinePlaceholder":228},[205,268,270,273,276,279,281],{"class":207,"line":269},8,[205,271,272],{"class":215},"site ",[205,274,275],{"class":211},"=",[205,277,278],{"class":215}," QgsCoordinateReferenceSystem.fromProj(",[205,280,236],{"class":235},[205,282,261],{"class":215},[205,284,286,289,292],{"class":207,"line":285},9,[205,287,288],{"class":211},"if",[205,290,291],{"class":211}," not",[205,293,294],{"class":215}," site.isValid():\n",[205,296,298,301,304,307,310],{"class":207,"line":297},10,[205,299,300],{"class":211},"    raise",[205,302,303],{"class":235}," ValueError",[205,305,306],{"class":215},"(",[205,308,309],{"class":248},"\"PROJ rejected the definition\"",[205,311,261],{"class":215},[205,313,315],{"class":207,"line":314},11,[205,316,229],{"emptyLinePlaceholder":228},[205,318,320,323,325,328,331,334,337],{"class":207,"line":319},12,[205,321,322],{"class":235},"print",[205,324,306],{"class":215},[205,326,327],{"class":248},"\"description:\"",[205,329,330],{"class":215},", site.description() ",[205,332,333],{"class":211},"or",[205,335,336],{"class":248}," \"(unnamed)\"",[205,338,261],{"class":215},[205,340,342,344,346,349,352,355],{"class":207,"line":341},13,[205,343,322],{"class":235},[205,345,306],{"class":215},[205,347,348],{"class":248},"\"geographic?\"",[205,350,351],{"class":215},", site.isGeographic(), ",[205,353,354],{"class":248},"\"units:\"",[205,356,357],{"class":215},", site.mapUnits())\n",[205,359,361,363],{"class":207,"line":360},14,[205,362,322],{"class":235},[205,364,365],{"class":215},"(site.toWkt(Qgis.CrsWktVariant.Wkt2_2019Simplified))\n",[14,367,368,371,372,375,376,379,380,383],{},[162,369,370],{},"Breakdown:"," A valid CRS is only one PROJ accepted, not one that is correct — a typo in ",[173,373,374],{},"lon_0"," still produces a valid, wrong system. ",[173,377,378],{},"towgs84"," with seven zeros states that the datum is equivalent to WGS 84 for transformation purposes; omit it and PROJ may warn that the datum shift is unknown, and transformations to other datums become ballpark only. Printing WKT2 shows how PROJ interpreted every parameter, which is the quickest way to spot a misread value: an origin latitude of ",[173,381,382],{},"51.5"," should appear as such, not as radians. WKT2 is also the form to store and share, because it carries the datum and axis order explicitly where a PROJ string relies on conventions.",[14,385,386,387,390,391,394],{},"Surveyors rarely hand over a PROJ string. More often the parameters arrive as a table in a PDF — \"central meridian 0°07′12″ W, scale factor 1.00002, false origin 5,000 m E, 10,000 m N\" — and the translation is where errors creep in. Convert sexagesimal angles to decimal degrees with the sign carried through (west and south are negative), confirm whether the false origin applies at the natural origin or at a different latitude, and check whether \"scale factor\" means the projection's ",[173,388,389],{},"k"," or a combined scale-and-elevation factor applied afterwards. When the documentation names a projection method, look up its exact PROJ name rather than assuming ",[173,392,393],{},"tmerc",": an oblique stereographic or a Lambert conformal conic behaves completely differently with the same numbers.",[151,396,398],{"id":397},"validate-against-a-control-point","Validate against a control point",[14,400,401],{},"The only real test of a custom CRS is a point whose coordinates are known in both systems. Transform one into the other and compare.",[14,403,404],{},[29,405,408,411,414,417,432,436,440,446,451,455,461,464,469,474,478,482,486,489,495,501,505,509,512,515,518,522],{"viewBox":406,"role":32,"ariaLabel":407,"xmlns":34},"0 0 760 256","Checking a custom CRS with a control point: a survey pin with published site grid coordinates and WGS 84 coordinates is transformed from WGS 84 into the custom CRS, and the difference between computed and published coordinates shows whether the definition is right",[36,409,410],{},"A control point settles it",[40,412,413],{},"A survey control pin has published coordinates in the site grid, 5,231.418 east and 10,412.906 north, and in WGS 84. The script transforms the WGS 84 coordinates into the custom CRS and compares. A difference of 3 millimetres passes. A difference of 72 metres fails and usually means a wrong origin or false easting. A difference of about 100 metres in a consistent direction usually means a missing datum shift.",[44,415],{"x":46,"y":46,"width":47,"height":416,"fill":49},"256",[418,419,420],"defs",{},[421,422,428],"marker",{"id":423,"viewBox":424,"refX":65,"refY":425,"markerWidth":426,"markerHeight":426,"orient":427},"ccrsCtlArrow","0 0 10 10","5","7","auto-start-reverse",[429,430],"path",{"d":431,"fill":73},"M0 0 L10 5 L0 10 z",[51,433,435],{"x":53,"y":434,"style":55,"fill":56,"textAnchor":57},"26","Compute, compare, then trust",[44,437],{"x":61,"y":438,"width":109,"height":439,"rx":79,"fill":66,"stroke":67,"style":68},"52","90",[51,441,445],{"x":442,"y":443,"style":444,"fill":56,"textAnchor":57},"124","78","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","control pin CP7",[51,447,450],{"x":442,"y":448,"style":449,"fill":73,"textAnchor":57},"102","text-anchor:middle;font-size:9.5px;font-family:monospace","WGS 84 lat\u002Flon",[51,452,454],{"x":442,"y":453,"style":449,"fill":73,"textAnchor":57},"122","grid E\u002FN published",[207,456],{"x1":457,"y1":458,"x2":459,"y2":458,"stroke":73,"style":460},"224","97","266","stroke-width:1.8;marker-end:url(#ccrsCtlArrow)",[44,462],{"x":463,"y":438,"width":109,"height":439,"rx":79,"fill":80,"stroke":81,"style":68},"274",[51,465,468],{"x":466,"y":467,"style":444,"fill":81,"textAnchor":57},"374","86","transform",[51,470,473],{"x":466,"y":471,"style":472,"fill":73,"textAnchor":57},"110","text-anchor:middle;font-size:10px;font-family:sans-serif","WGS 84 → site grid",[207,475],{"x1":476,"y1":458,"x2":477,"y2":458,"stroke":73,"style":460},"474","516",[44,479],{"x":480,"y":438,"width":481,"height":439,"rx":79,"fill":66,"stroke":67,"style":68},"524","212",[51,483,485],{"x":484,"y":467,"style":444,"fill":56,"textAnchor":57},"630","difference",[51,487,488],{"x":484,"y":471,"style":472,"fill":73,"textAnchor":57},"computed − published",[44,490],{"x":61,"y":491,"width":124,"height":492,"rx":79,"fill":493,"stroke":494,"style":68},"164","72","#e8efe6","#15803d",[51,496,500],{"x":497,"y":498,"style":444,"fill":499,"textAnchor":57},"135","194","#166534","3 mm",[51,502,504],{"x":497,"y":503,"style":472,"fill":73,"textAnchor":57},"214","definition correct",[44,506],{"x":507,"y":491,"width":124,"height":492,"rx":79,"fill":135,"stroke":508,"style":68},"269","#b91c1c",[51,510,511],{"x":53,"y":498,"style":444,"fill":508,"textAnchor":57},"72 m",[51,513,514],{"x":53,"y":503,"style":472,"fill":73,"textAnchor":57},"origin or false offset wrong",[44,516],{"x":517,"y":491,"width":124,"height":492,"rx":79,"fill":135,"stroke":136,"style":68},"514",[51,519,521],{"x":520,"y":498,"style":444,"fill":136,"textAnchor":57},"625","~100 m, one direction",[51,523,524],{"x":520,"y":503,"style":472,"fill":73,"textAnchor":57},"datum shift missing",[196,526,528],{"className":198,"code":527,"language":200,"meta":201,"style":201},"from qgis.core import QgsCoordinateTransform, QgsPointXY, QgsProject\n\nwgs84 = QgsCoordinateReferenceSystem(\"EPSG:4326\")\nto_site = QgsCoordinateTransform(wgs84, site, QgsProject.instance())\n\nCONTROL = [\n    # name, lon, lat, published E, published N\n    (\"CP7\", -0.119162, 51.502181, 5231.418, 10412.906),\n    (\"CP9\", -0.123840, 51.498774, 4906.221, 10033.500),\n]\n\nfor name, lon, lat, e, n in CONTROL:\n    p = to_site.transform(QgsPointXY(lon, lat))\n    de, dn = p.x() - e, p.y() - n\n    print(f\"{name}: ΔE {de:+.3f} m  ΔN {dn:+.3f} m  → {(de**2 + dn**2) ** 0.5:.3f} m\")\n",[173,529,530,541,545,560,570,574,584,590,625,656,661,665,682,692,712],{"__ignoreMap":201},[205,531,532,534,536,538],{"class":207,"line":208},[205,533,212],{"class":211},[205,535,216],{"class":215},[205,537,219],{"class":211},[205,539,540],{"class":215}," QgsCoordinateTransform, QgsPointXY, QgsProject\n",[205,542,543],{"class":207,"line":225},[205,544,229],{"emptyLinePlaceholder":228},[205,546,547,550,552,555,558],{"class":207,"line":232},[205,548,549],{"class":215},"wgs84 ",[205,551,275],{"class":211},[205,553,554],{"class":215}," QgsCoordinateReferenceSystem(",[205,556,557],{"class":248},"\"EPSG:4326\"",[205,559,261],{"class":215},[205,561,562,565,567],{"class":207,"line":245},[205,563,564],{"class":215},"to_site ",[205,566,275],{"class":211},[205,568,569],{"class":215}," QgsCoordinateTransform(wgs84, site, QgsProject.instance())\n",[205,571,572],{"class":207,"line":252},[205,573,229],{"emptyLinePlaceholder":228},[205,575,576,579,581],{"class":207,"line":258},[205,577,578],{"class":235},"CONTROL",[205,580,239],{"class":211},[205,582,583],{"class":215}," [\n",[205,585,586],{"class":207,"line":264},[205,587,589],{"class":588},"sjoCn","    # name, lon, lat, published E, published N\n",[205,591,592,595,598,601,604,607,609,612,614,617,619,622],{"class":207,"line":269},[205,593,594],{"class":215},"    (",[205,596,597],{"class":248},"\"CP7\"",[205,599,600],{"class":215},", ",[205,602,603],{"class":211},"-",[205,605,606],{"class":235},"0.119162",[205,608,600],{"class":215},[205,610,611],{"class":235},"51.502181",[205,613,600],{"class":215},[205,615,616],{"class":235},"5231.418",[205,618,600],{"class":215},[205,620,621],{"class":235},"10412.906",[205,623,624],{"class":215},"),\n",[205,626,627,629,632,634,636,639,641,644,646,649,651,654],{"class":207,"line":285},[205,628,594],{"class":215},[205,630,631],{"class":248},"\"CP9\"",[205,633,600],{"class":215},[205,635,603],{"class":211},[205,637,638],{"class":235},"0.123840",[205,640,600],{"class":215},[205,642,643],{"class":235},"51.498774",[205,645,600],{"class":215},[205,647,648],{"class":235},"4906.221",[205,650,600],{"class":215},[205,652,653],{"class":235},"10033.500",[205,655,624],{"class":215},[205,657,658],{"class":207,"line":297},[205,659,660],{"class":215},"]\n",[205,662,663],{"class":207,"line":314},[205,664,229],{"emptyLinePlaceholder":228},[205,666,667,670,673,676,679],{"class":207,"line":319},[205,668,669],{"class":211},"for",[205,671,672],{"class":215}," name, lon, lat, e, n ",[205,674,675],{"class":211},"in",[205,677,678],{"class":235}," CONTROL",[205,680,681],{"class":215},":\n",[205,683,684,687,689],{"class":207,"line":341},[205,685,686],{"class":215},"    p ",[205,688,275],{"class":211},[205,690,691],{"class":215}," to_site.transform(QgsPointXY(lon, lat))\n",[205,693,694,697,699,702,704,707,709],{"class":207,"line":360},[205,695,696],{"class":215},"    de, dn ",[205,698,275],{"class":211},[205,700,701],{"class":215}," p.x() ",[205,703,603],{"class":211},[205,705,706],{"class":215}," e, p.y() ",[205,708,603],{"class":211},[205,710,711],{"class":215}," n\n",[205,713,715,718,720,723,726,729,732,735,738,740,743,746,748,751,753,756,758,760,763,765,768,771,774,777,780,782,784,787,789,792,795,797,800],{"class":207,"line":714},15,[205,716,717],{"class":235},"    print",[205,719,306],{"class":215},[205,721,722],{"class":211},"f",[205,724,725],{"class":248},"\"",[205,727,728],{"class":235},"{",[205,730,731],{"class":215},"name",[205,733,734],{"class":235},"}",[205,736,737],{"class":248},": ΔE ",[205,739,728],{"class":235},[205,741,742],{"class":215},"de",[205,744,745],{"class":211},":+.3f",[205,747,734],{"class":235},[205,749,750],{"class":248}," m  ΔN ",[205,752,728],{"class":235},[205,754,755],{"class":215},"dn",[205,757,745],{"class":211},[205,759,734],{"class":235},[205,761,762],{"class":248}," m  → ",[205,764,728],{"class":235},[205,766,767],{"class":215},"(de",[205,769,770],{"class":211},"**",[205,772,773],{"class":235},"2",[205,775,776],{"class":211}," +",[205,778,779],{"class":215}," dn",[205,781,770],{"class":211},[205,783,773],{"class":235},[205,785,786],{"class":215},") ",[205,788,770],{"class":211},[205,790,791],{"class":235}," 0.5",[205,793,794],{"class":211},":.3f",[205,796,734],{"class":235},[205,798,799],{"class":248}," m\"",[205,801,261],{"class":215},[14,803,804,806,807,810,811,815],{},[162,805,370],{}," Remember that ",[173,808,809],{},"QgsPointXY"," takes x then y, so for WGS 84 that is longitude before latitude. Two or more control points spread across the site catch errors a single point cannot: a scale factor error grows with distance from the origin, and a rotation shows as residuals changing direction between points. Residuals of a few millimetres to a centimetre are what a correct definition gives; tens of metres point at a wrong origin or false easting; a consistent offset of around a hundred metres is the classic signature of a missing or wrong datum shift. Transform basics are covered in ",[21,812,814],{"href":813},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002F","transforming point coordinates between CRSs",".",[151,817,819],{"id":818},"register-it-as-a-user-crs","Register it as a user CRS",[14,821,822],{},"A CRS object in a script is enough to assign it to layers in that script. To make it selectable in QGIS's CRS dialog — and reusable by name — register it in the user CRS database.",[196,824,826],{"className":198,"code":825,"language":200,"meta":201,"style":201},"from qgis.core import QgsApplication\n\nregistry = QgsApplication.coordinateReferenceSystemRegistry()\nname = \"Riverside Site Grid 2026\"\n\nexisting = [d for d in registry.userCrsList() if d.name == name]\nif existing:\n    site = QgsCoordinateReferenceSystem(f\"USER:{existing[0].id}\")\nelse:\n    crs_id = registry.addUserCrs(site, name, Qgis.CrsDefinitionFormat.Wkt)\n    if crs_id == -1:\n        raise RuntimeError(\"could not add user CRS\")\n    site = QgsCoordinateReferenceSystem(f\"USER:{crs_id}\")\n\nprint(site.authid(), site.description())\n",[173,827,828,839,843,853,863,867,898,905,935,942,952,970,985,1008,1012],{"__ignoreMap":201},[205,829,830,832,834,836],{"class":207,"line":208},[205,831,212],{"class":211},[205,833,216],{"class":215},[205,835,219],{"class":211},[205,837,838],{"class":215}," QgsApplication\n",[205,840,841],{"class":207,"line":225},[205,842,229],{"emptyLinePlaceholder":228},[205,844,845,848,850],{"class":207,"line":232},[205,846,847],{"class":215},"registry ",[205,849,275],{"class":211},[205,851,852],{"class":215}," QgsApplication.coordinateReferenceSystemRegistry()\n",[205,854,855,858,860],{"class":207,"line":245},[205,856,857],{"class":215},"name ",[205,859,275],{"class":211},[205,861,862],{"class":248}," \"Riverside Site Grid 2026\"\n",[205,864,865],{"class":207,"line":252},[205,866,229],{"emptyLinePlaceholder":228},[205,868,869,872,874,877,879,882,884,887,889,892,895],{"class":207,"line":258},[205,870,871],{"class":215},"existing ",[205,873,275],{"class":211},[205,875,876],{"class":215}," [d ",[205,878,669],{"class":211},[205,880,881],{"class":215}," d ",[205,883,675],{"class":211},[205,885,886],{"class":215}," registry.userCrsList() ",[205,888,288],{"class":211},[205,890,891],{"class":215}," d.name ",[205,893,894],{"class":211},"==",[205,896,897],{"class":215}," name]\n",[205,899,900,902],{"class":207,"line":264},[205,901,288],{"class":211},[205,903,904],{"class":215}," existing:\n",[205,906,907,910,912,914,916,919,921,924,926,929,931,933],{"class":207,"line":269},[205,908,909],{"class":215},"    site ",[205,911,275],{"class":211},[205,913,554],{"class":215},[205,915,722],{"class":211},[205,917,918],{"class":248},"\"USER:",[205,920,728],{"class":235},[205,922,923],{"class":215},"existing[",[205,925,46],{"class":235},[205,927,928],{"class":215},"].id",[205,930,734],{"class":235},[205,932,725],{"class":248},[205,934,261],{"class":215},[205,936,937,940],{"class":207,"line":285},[205,938,939],{"class":211},"else",[205,941,681],{"class":215},[205,943,944,947,949],{"class":207,"line":297},[205,945,946],{"class":215},"    crs_id ",[205,948,275],{"class":211},[205,950,951],{"class":215}," registry.addUserCrs(site, name, Qgis.CrsDefinitionFormat.Wkt)\n",[205,953,954,957,960,962,965,968],{"class":207,"line":314},[205,955,956],{"class":211},"    if",[205,958,959],{"class":215}," crs_id ",[205,961,894],{"class":211},[205,963,964],{"class":211}," -",[205,966,967],{"class":235},"1",[205,969,681],{"class":215},[205,971,972,975,978,980,983],{"class":207,"line":319},[205,973,974],{"class":211},"        raise",[205,976,977],{"class":235}," RuntimeError",[205,979,306],{"class":215},[205,981,982],{"class":248},"\"could not add user CRS\"",[205,984,261],{"class":215},[205,986,987,989,991,993,995,997,999,1002,1004,1006],{"class":207,"line":341},[205,988,909],{"class":215},[205,990,275],{"class":211},[205,992,554],{"class":215},[205,994,722],{"class":211},[205,996,918],{"class":248},[205,998,728],{"class":235},[205,1000,1001],{"class":215},"crs_id",[205,1003,734],{"class":235},[205,1005,725],{"class":248},[205,1007,261],{"class":215},[205,1009,1010],{"class":207,"line":360},[205,1011,229],{"emptyLinePlaceholder":228},[205,1013,1014,1016],{"class":207,"line":714},[205,1015,322],{"class":235},[205,1017,1018],{"class":215},"(site.authid(), site.description())\n",[14,1020,1021,1023,1024,1027,1028,1031,1032,1036],{},[162,1022,370],{}," User CRSs are stored in the QGIS profile's ",[173,1025,1026],{},"qgis.db"," and get ids starting at 100000, referenced as ",[173,1029,1030],{},"USER:100000",". Checking by name first keeps the script idempotent — running it twice does not register two copies. Storing the definition as WKT rather than PROJ keeps datum and axis information intact. Once registered, the CRS appears under ",[1033,1034,1035],"em",{},"User-defined Coordinate Systems"," in every CRS selector, so colleagues on the same profile can pick it by name.",[14,1038,1039],{},[29,1040,1043,1046,1049,1052,1055,1059,1065,1069,1072,1076,1079,1082,1085,1088,1091,1093,1097,1099,1102,1105],{"viewBox":1041,"role":32,"ariaLabel":1042,"xmlns":34},"0 0 760 244","Why USER ids are not portable: a project saved on one machine references USER:100003, but the full WKT is also stored in the project, so another machine without that user CRS still opens the layer correctly, while scripts that look up USER:100003 by id would find a different or missing CRS",[36,1044,1045],{},"Store the definition, not the id",[40,1047,1048],{},"Machine A registered the site grid as USER:100003 and saved a project. The project file contains both the id and the full WKT definition. Machine B has a different user CRS at 100003. When B opens the project, QGIS matches on the stored definition and the layer is correct. A script on B that constructs QgsCoordinateReferenceSystem from the string USER:100003 gets the wrong CRS.",[44,1050],{"x":46,"y":46,"width":47,"height":1051,"fill":49},"244",[51,1053,1054],{"x":53,"y":434,"style":55,"fill":56,"textAnchor":57},"USER ids are local; WKT travels",[44,1056],{"x":61,"y":62,"width":1057,"height":1058,"rx":79,"fill":66,"stroke":67,"style":68},"220","176",[51,1060,1064],{"x":1061,"y":1062,"style":1063,"fill":56,"textAnchor":57},"134","74","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","machine A",[51,1066,1068],{"x":1061,"y":1067,"style":92,"fill":73,"textAnchor":57},"104","USER:100003",[51,1070,1071],{"x":1061,"y":442,"style":472,"fill":73,"textAnchor":57},"= Riverside grid",[51,1073,1075],{"x":1061,"y":1074,"style":472,"fill":67,"textAnchor":57},"180","saves project",[44,1077],{"x":1078,"y":62,"width":1057,"height":1058,"rx":79,"fill":80,"stroke":81,"style":68},"270",[51,1080,1081],{"x":53,"y":1062,"style":1063,"fill":81,"textAnchor":57},"project file",[51,1083,1084],{"x":53,"y":1067,"style":92,"fill":73,"textAnchor":57},"authid USER:100003",[51,1086,1087],{"x":53,"y":442,"style":92,"fill":73,"textAnchor":57},"+ full WKT",[51,1089,1090],{"x":53,"y":1074,"style":472,"fill":499,"textAnchor":57},"definition travels",[44,1092],{"x":477,"y":62,"width":1057,"height":1058,"rx":79,"fill":66,"stroke":67,"style":68},[51,1094,1096],{"x":1095,"y":1062,"style":1063,"fill":56,"textAnchor":57},"626","machine B",[51,1098,1068],{"x":1095,"y":1067,"style":92,"fill":73,"textAnchor":57},[51,1100,1101],{"x":1095,"y":442,"style":472,"fill":73,"textAnchor":57},"= someone else's CRS",[51,1103,1104],{"x":1095,"y":491,"style":472,"fill":499,"textAnchor":57},"project opens correctly",[51,1106,1107],{"x":1095,"y":96,"style":472,"fill":508,"textAnchor":57},"script by id: wrong CRS",[151,1109,1111],{"id":1110},"keep-it-portable","Keep it portable",[14,1113,1114],{},"User CRS ids are local to one QGIS profile. The same id on another machine may be a different system or nothing at all. Projects are protected because they store the full definition alongside the id; scripts and shared files are not, unless you make them so.",[196,1116,1118],{"className":198,"code":1117,"language":200,"meta":201,"style":201},"from pathlib import Path\nfrom qgis.core import QgsVectorLayer\n\nwkt_file = Path(\"\u002Fsrv\u002Fgis\u002Fcrs\u002Friverside_site_grid.wkt\")\nwkt_file.write_text(site.toWkt(Qgis.CrsWktVariant.Wkt2_2019))\n\nshared = QgsCoordinateReferenceSystem.fromWkt(wkt_file.read_text())\nassert shared.isValid()\n\nsetting_out = QgsVectorLayer(\"\u002Fdata\u002Fsite\u002Fsetting_out_points.gpkg\", \"setting out\", \"ogr\")\nif not setting_out.crs().isValid():\n    setting_out.setCrs(shared)\n    setting_out.saveDefaultStyle()\n",[173,1119,1120,1132,1143,1147,1162,1167,1171,1181,1189,1193,1218,1227,1232],{"__ignoreMap":201},[205,1121,1122,1124,1127,1129],{"class":207,"line":208},[205,1123,212],{"class":211},[205,1125,1126],{"class":215}," pathlib ",[205,1128,219],{"class":211},[205,1130,1131],{"class":215}," Path\n",[205,1133,1134,1136,1138,1140],{"class":207,"line":225},[205,1135,212],{"class":211},[205,1137,216],{"class":215},[205,1139,219],{"class":211},[205,1141,1142],{"class":215}," QgsVectorLayer\n",[205,1144,1145],{"class":207,"line":232},[205,1146,229],{"emptyLinePlaceholder":228},[205,1148,1149,1152,1154,1157,1160],{"class":207,"line":245},[205,1150,1151],{"class":215},"wkt_file ",[205,1153,275],{"class":211},[205,1155,1156],{"class":215}," Path(",[205,1158,1159],{"class":248},"\"\u002Fsrv\u002Fgis\u002Fcrs\u002Friverside_site_grid.wkt\"",[205,1161,261],{"class":215},[205,1163,1164],{"class":207,"line":252},[205,1165,1166],{"class":215},"wkt_file.write_text(site.toWkt(Qgis.CrsWktVariant.Wkt2_2019))\n",[205,1168,1169],{"class":207,"line":258},[205,1170,229],{"emptyLinePlaceholder":228},[205,1172,1173,1176,1178],{"class":207,"line":264},[205,1174,1175],{"class":215},"shared ",[205,1177,275],{"class":211},[205,1179,1180],{"class":215}," QgsCoordinateReferenceSystem.fromWkt(wkt_file.read_text())\n",[205,1182,1183,1186],{"class":207,"line":269},[205,1184,1185],{"class":211},"assert",[205,1187,1188],{"class":215}," shared.isValid()\n",[205,1190,1191],{"class":207,"line":285},[205,1192,229],{"emptyLinePlaceholder":228},[205,1194,1195,1198,1200,1203,1206,1208,1211,1213,1216],{"class":207,"line":297},[205,1196,1197],{"class":215},"setting_out ",[205,1199,275],{"class":211},[205,1201,1202],{"class":215}," QgsVectorLayer(",[205,1204,1205],{"class":248},"\"\u002Fdata\u002Fsite\u002Fsetting_out_points.gpkg\"",[205,1207,600],{"class":215},[205,1209,1210],{"class":248},"\"setting out\"",[205,1212,600],{"class":215},[205,1214,1215],{"class":248},"\"ogr\"",[205,1217,261],{"class":215},[205,1219,1220,1222,1224],{"class":207,"line":314},[205,1221,288],{"class":211},[205,1223,291],{"class":211},[205,1225,1226],{"class":215}," setting_out.crs().isValid():\n",[205,1228,1229],{"class":207,"line":319},[205,1230,1231],{"class":215},"    setting_out.setCrs(shared)\n",[205,1233,1234],{"class":207,"line":341},[205,1235,1236],{"class":215},"    setting_out.saveDefaultStyle()\n",[14,1238,1239,1241,1242,1245,1246,1249,1250,1254],{},[162,1240,370],{}," Keeping the WKT in a file under version control, next to the scripts that use it, makes the definition the shared source of truth rather than any one person's profile. Scripts build the CRS from that file every time, never from a ",[173,1243,1244],{},"USER:"," id. GeoPackage stores a layer's CRS definition inside the file, so data written in the custom CRS carries it along; shapefiles store it in a ",[173,1247,1248],{},".prj"," sidecar that is easy to lose. The fallback assignment covers files delivered without a CRS, as discussed in ",[21,1251,1253],{"href":1252},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fhandling-missing-crs-in-pyqgis\u002F","handling missing CRS",". If the organisation uses the system widely, the lasting fix is to ask the surveyors whether it can be registered with EPSG or published as a named definition in a shared PROJ database.",[151,1256,1258],{"id":1257},"qgis-version-compatibility","QGIS version compatibility",[14,1260,1261,1264,1265,1268,1269,1272,1273,1276,1277,1280,1281,1284,1285,1288],{},[173,1262,1263],{},"fromProj"," arrived in QGIS 3.10 (earlier releases use ",[173,1266,1267],{},"createFromProj4","), and ",[173,1270,1271],{},"coordinateReferenceSystemRegistry().addUserCrs"," in 3.18 (earlier releases use ",[173,1274,1275],{},"saveAsUserCrs","). ",[173,1278,1279],{},"Qgis.CrsWktVariant"," replaced ",[173,1282,1283],{},"QgsCoordinateReferenceSystem.WKT2_2019"," in 3.36, and ",[173,1286,1287],{},"Qgis.CrsDefinitionFormat"," dates from 3.24. The QGIS 4 series accepts only the scoped enums. PROJ versions bundled with installers may interpret edge-case PROJ strings slightly differently, which is another reason to store WKT2.",[151,1290,1292],{"id":1291},"troubleshooting","Troubleshooting",[156,1294,1295,1303,1309,1318,1327],{},[159,1296,1297,1302],{},[162,1298,1299,1301],{},[173,1300,193],{}," is False."," A parameter name is misspelt or a value is not a number; PROJ rejects the whole string.",[159,1304,1305,1308],{},[162,1306,1307],{},"Everything is offset by roughly 100 m."," The datum shift is missing or wrong.",[159,1310,1311,1314,1315,1317],{},[162,1312,1313],{},"Offsets grow with distance from the origin."," The scale factor ",[173,1316,389],{}," is wrong or omitted.",[159,1319,1320,1323,1324,1326],{},[162,1321,1322],{},"The CRS is missing on a colleague's machine."," A ",[173,1325,1244],{}," id was used; share the WKT instead.",[159,1328,1329,1332,1333,1336],{},[162,1330,1331],{},"Coordinates are swapped."," Axis order differs between the WKT you imported and the data; check the ",[173,1334,1335],{},"AXIS"," entries in WKT2.",[151,1338,1340],{"id":1339},"conclusion","Conclusion",[14,1342,1343,1344,1346,1347,1349],{},"Search the registry before inventing a CRS. When one is genuinely needed, build it from the surveyor's parameters with ",[173,1345,1263],{}," or ",[173,1348,189],{},", prove it against two or more control points, register it as a user CRS for convenience, and share the WKT2 definition — not the local id — with every script and colleague that needs it.",[151,1351,1353],{"id":1352},"frequently-asked-questions","Frequently Asked Questions",[14,1355,1356,1359],{},[162,1357,1358],{},"Can I create a local engineering grid with no link to the earth?","\nYes, as an engineering CRS in WKT, but it cannot be transformed to or from any geographic CRS. Only use one when the data will never meet real-world layers.",[14,1361,1362,1365],{},[162,1363,1364],{},"What is a low-distortion projection and do I need one?","\nA projection centred on the project area with a scale factor chosen so grid distances match ground distances. It matters where engineering measurements from the map must agree with tape measurements on site.",[14,1367,1368,1371],{},[162,1369,1370],{},"Does a custom CRS work in QGIS Server and qgis_process?","\nYes, if the definition is available — in the project file, the data file, or built from WKT in the script.",[14,1373,1374,1377],{},[162,1375,1376],{},"How do I add a datum grid transformation to a custom CRS?","\nReference the grid in the PROJ pipeline or the WKT's bound CRS, and make sure the grid file is installed in the PROJ data directory on every machine that uses it.",[151,1379,1381],{"id":1380},"related","Related",[156,1383,1384,1389,1395,1400,1406],{},[159,1385,1386,1388],{},[21,1387,24],{"href":23}," — the guide this recipe belongs to",[159,1390,1391],{},[21,1392,1394],{"href":1393},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fchoose-projected-crs-for-analysis-pyqgis\u002F","Choose a Projected CRS for Analysis in PyQGIS",[159,1396,1397],{},[21,1398,1399],{"href":1252},"Handling Missing CRS in PyQGIS",[159,1401,1402],{},[21,1403,1405],{"href":1404},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fset-project-crs-and-datum-transform-pyqgis\u002F","Set the Project CRS and Datum Transform in PyQGIS",[159,1407,1408],{},[21,1409,1410],{"href":813},"Transform Point Coordinates Between CRS in PyQGIS",[1412,1413,1414],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":201,"searchDepth":225,"depth":225,"links":1416},[1417,1418,1419,1420,1421,1422,1423,1424,1425,1426],{"id":153,"depth":225,"text":154},{"id":179,"depth":225,"text":180},{"id":397,"depth":225,"text":398},{"id":818,"depth":225,"text":819},{"id":1110,"depth":225,"text":1111},{"id":1257,"depth":225,"text":1258},{"id":1291,"depth":225,"text":1292},{"id":1339,"depth":225,"text":1340},{"id":1352,"depth":225,"text":1353},{"id":1380,"depth":225,"text":1381},"Define a coordinate reference system that is not in the EPSG database — a local site grid, a custom transverse Mercator or an engineering system — from a PROJ string or WKT, validate it with a control point, register it as a user CRS and keep projects portable across machines.","md",{"slug":1430,"type":1431,"breadcrumb":1432,"datePublished":1433,"dateModified":1433},"create-custom-crs-from-proj-string-pyqgis","article","Create a Custom CRS","2026-09-17","\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fcreate-custom-crs-from-proj-string-pyqgis",{"title":5,"description":1427},"spatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fcreate-custom-crs-from-proj-string-pyqgis\u002Findex","J7Ou0Wos9gxdaZBZuT9u0jvBvlEPUjHRwcaxr3FBuAM",1789632908266]