[{"data":1,"prerenderedAt":1832},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fimport-geotagged-photos-as-points-pyqgis":3},{"id":4,"title":5,"body":6,"description":1821,"extension":1822,"meta":1823,"navigation":263,"path":1828,"seo":1829,"stem":1830,"__hash__":1831},"docs\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fimport-geotagged-photos-as-points-pyqgis\u002Findex.md","Import Geotagged Photos as Points in PyQGIS",{"type":7,"value":8,"toc":1808},"minimark",[9,13,17,26,193,198,215,219,222,436,463,467,474,577,868,880,884,891,1121,1130,1230,1234,1237,1359,1377,1381,1388,1642,1663,1667,1681,1685,1722,1726,1738,1742,1748,1754,1760,1769,1773,1804],[10,11,5],"h1",{"id":12},"import-geotagged-photos-as-points-in-pyqgis",[14,15,16],"p",{},"Site surveys, asset inspections and tree audits all produce the same deliverable: a folder of a few hundred JPEGs taken on a phone or a GPS camera. Each file carries its own location in its EXIF header, so the map is already in the folder — it just needs reading out. QGIS ships an algorithm for exactly that, and wrapping it in a script makes it repeatable for every survey day.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002F","Layer Data Sources & Formats in PyQGIS",". It runs the import, deals with the photos that have no position, turns the camera direction into a rotated symbol, and puts the photo itself into the map tip.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,98,102,106,110,114,118,123,129,138,144,148,152,158,163,169,173,177,181,185,189],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 300","img","A JPEG's EXIF header feeding the photo import algorithm: GPS latitude, longitude and altitude become the point, GPS image direction becomes a direction field, and the original timestamp becomes a timestamp field","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"From EXIF tags to attributes",[40,41,42],"desc",{},"On the left, a photo file with an EXIF block listing GPSLatitude, GPSLongitude, GPSAltitude, GPSImgDirection and DateTimeOriginal. Arrows lead to the native importphotos algorithm and then to a point layer in WGS 84 with PointZ geometry and fields photo, filename, directory, altitude, direction, rotation, longitude, latitude and timestamp.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","300","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"photoExifArrow","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","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The location is already inside every file",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"24","50","236","226","10","#fffdf7","#59645f","stroke-width:2",[68,87,91],{"x":88,"y":89,"style":90,"fill":73,"textAnchor":74},"142","74","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","IMG_4172.jpg · EXIF",[68,93,97],{"x":94,"y":95,"style":96,"fill":66},"42","106","font-size:9.5px;font-family:monospace","GPSLatitude    53.80082",[68,99,101],{"x":94,"y":100,"style":96,"fill":66},"132","GPSLongitude   -1.54911",[68,103,105],{"x":94,"y":104,"style":96,"fill":66},"158","GPSAltitude    74.2",[68,107,109],{"x":94,"y":108,"style":96,"fill":66},"184","GPSImgDirection 212",[68,111,113],{"x":94,"y":112,"style":96,"fill":66},"210","DateTimeOriginal",[68,115,117],{"x":94,"y":116,"style":96,"fill":66},"228","  2026:09:14 10:31",[68,119,122],{"x":88,"y":120,"style":121,"fill":84,"textAnchor":74},"258","text-anchor:middle;font-size:9.5px;font-family:sans-serif","written by the camera",[124,125],"line",{"x1":126,"y1":127,"x2":48,"y2":127,"stroke":66,"style":128},"260","162","stroke-width:2;marker-end:url(#photoExifArrow)",[44,130],{"x":131,"y":132,"width":133,"height":134,"rx":82,"fill":135,"stroke":136,"style":137},"308","118","150","88","#eef7f4","#0f766e","stroke-width:2.5",[68,139,143],{"x":140,"y":141,"style":142,"fill":136,"textAnchor":74},"383","152","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","native:",[68,145,147],{"x":140,"y":146,"style":142,"fill":136,"textAnchor":74},"172","importphotos",[124,149],{"x1":150,"y1":127,"x2":151,"y2":127,"stroke":66,"style":128},"458","498",[44,153],{"x":154,"y":79,"width":155,"height":81,"rx":82,"fill":156,"stroke":157,"style":85},"506","230","#e8efe6","#15803d",[68,159,162],{"x":160,"y":89,"style":90,"fill":161,"textAnchor":74},"621","#166534","PointZ · EPSG:4326",[68,164,168],{"x":165,"y":166,"style":167,"fill":66},"526","104","font-size:10px;font-family:monospace","photo       full path",[68,170,172],{"x":165,"y":171,"style":167,"fill":66},"126","filename",[68,174,176],{"x":165,"y":175,"style":167,"fill":66},"148","altitude    74.2",[68,178,180],{"x":165,"y":179,"style":167,"fill":66},"170","direction   212",[68,182,184],{"x":165,"y":183,"style":167,"fill":66},"192","rotation    0",[68,186,188],{"x":165,"y":187,"style":167,"fill":66},"214","timestamp   datetime",[68,190,192],{"x":160,"y":191,"style":121,"fill":84,"textAnchor":74},"252","plus longitude, latitude, directory",[194,195,197],"h2",{"id":196},"prerequisites","Prerequisites",[199,200,201,209,212],"ul",{},[202,203,204,208],"li",{},[205,206,207],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series. The algorithm has existed since 3.6.",[202,210,211],{},"A folder of JPEG or TIFF images with GPS tags. Phones write them when location access is enabled for the camera app; many dedicated cameras need an external GPS unit.",[202,213,214],{},"A projected CRS for your survey area if you intend to measure anything afterwards.",[194,216,218],{"id":217},"run-the-import","Run the import",[14,220,221],{},"The algorithm walks a folder, reads each image's EXIF block, and writes one point per photo that has a usable position. A second, optional output lists the files it could not place.",[223,224,229],"pre",{"className":225,"code":226,"language":227,"meta":228,"style":228},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject, QgsVectorLayer\n\nsurvey_day = \"\u002Fdata\u002Fsurveys\u002F2026-09-14\"\n\nresult = processing.run(\"native:importphotos\", {\n    \"FOLDER\": survey_day,\n    \"RECURSIVE\": True,\n    \"OUTPUT\": f\"{survey_day}\u002Fphotos.gpkg\",\n    \"INVALID\": f\"{survey_day}\u002Fphotos_without_gps.csv\",\n})\n\nphotos = QgsVectorLayer(result[\"OUTPUT\"], \"survey photos\", \"ogr\")\nprint(photos.featureCount(), \"photos placed\")\nQgsProject.instance().addMapLayer(photos)\n","python","",[230,231,232,244,258,265,278,283,300,309,325,353,376,382,387,416,430],"code",{"__ignoreMap":228},[233,234,236,240],"span",{"class":124,"line":235},1,[233,237,239],{"class":238},"snl16","import",[233,241,243],{"class":242},"s95oV"," processing\n",[233,245,247,250,253,255],{"class":124,"line":246},2,[233,248,249],{"class":238},"from",[233,251,252],{"class":242}," qgis.core ",[233,254,239],{"class":238},[233,256,257],{"class":242}," QgsProject, QgsVectorLayer\n",[233,259,261],{"class":124,"line":260},3,[233,262,264],{"emptyLinePlaceholder":263},true,"\n",[233,266,268,271,274],{"class":124,"line":267},4,[233,269,270],{"class":242},"survey_day ",[233,272,273],{"class":238},"=",[233,275,277],{"class":276},"sU2Wk"," \"\u002Fdata\u002Fsurveys\u002F2026-09-14\"\n",[233,279,281],{"class":124,"line":280},5,[233,282,264],{"emptyLinePlaceholder":263},[233,284,286,289,291,294,297],{"class":124,"line":285},6,[233,287,288],{"class":242},"result ",[233,290,273],{"class":238},[233,292,293],{"class":242}," processing.run(",[233,295,296],{"class":276},"\"native:importphotos\"",[233,298,299],{"class":242},", {\n",[233,301,303,306],{"class":124,"line":302},7,[233,304,305],{"class":276},"    \"FOLDER\"",[233,307,308],{"class":242},": survey_day,\n",[233,310,312,315,318,322],{"class":124,"line":311},8,[233,313,314],{"class":276},"    \"RECURSIVE\"",[233,316,317],{"class":242},": ",[233,319,321],{"class":320},"sDLfK","True",[233,323,324],{"class":242},",\n",[233,326,328,331,333,336,339,342,345,348,351],{"class":124,"line":327},9,[233,329,330],{"class":276},"    \"OUTPUT\"",[233,332,317],{"class":242},[233,334,335],{"class":238},"f",[233,337,338],{"class":276},"\"",[233,340,341],{"class":320},"{",[233,343,344],{"class":242},"survey_day",[233,346,347],{"class":320},"}",[233,349,350],{"class":276},"\u002Fphotos.gpkg\"",[233,352,324],{"class":242},[233,354,356,359,361,363,365,367,369,371,374],{"class":124,"line":355},10,[233,357,358],{"class":276},"    \"INVALID\"",[233,360,317],{"class":242},[233,362,335],{"class":238},[233,364,338],{"class":276},[233,366,341],{"class":320},[233,368,344],{"class":242},[233,370,347],{"class":320},[233,372,373],{"class":276},"\u002Fphotos_without_gps.csv\"",[233,375,324],{"class":242},[233,377,379],{"class":124,"line":378},11,[233,380,381],{"class":242},"})\n",[233,383,385],{"class":124,"line":384},12,[233,386,264],{"emptyLinePlaceholder":263},[233,388,390,393,395,398,401,404,407,410,413],{"class":124,"line":389},13,[233,391,392],{"class":242},"photos ",[233,394,273],{"class":238},[233,396,397],{"class":242}," QgsVectorLayer(result[",[233,399,400],{"class":276},"\"OUTPUT\"",[233,402,403],{"class":242},"], ",[233,405,406],{"class":276},"\"survey photos\"",[233,408,409],{"class":242},", ",[233,411,412],{"class":276},"\"ogr\"",[233,414,415],{"class":242},")\n",[233,417,419,422,425,428],{"class":124,"line":418},14,[233,420,421],{"class":320},"print",[233,423,424],{"class":242},"(photos.featureCount(), ",[233,426,427],{"class":276},"\"photos placed\"",[233,429,415],{"class":242},[233,431,433],{"class":124,"line":432},15,[233,434,435],{"class":242},"QgsProject.instance().addMapLayer(photos)\n",[14,437,438,441,442,445,446,449,450,453,454,457,458,462],{},[205,439,440],{},"Breakdown:"," ",[230,443,444],{},"RECURSIVE"," descends into subfolders, which suits a survey where each crew uploads to its own directory. The output is a ",[230,447,448],{},"PointZ"," layer in WGS 84, because that is what GPS tags hold; the z value comes from ",[230,451,452],{},"GPSAltitude"," when present. The ",[230,455,456],{},"photo"," field stores the absolute path, which is what map tips and attribute forms use to display the image — and why moving the folder later breaks the links, a problem covered in ",[21,459,461],{"href":460},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Ffix-broken-layer-paths-in-project-pyqgis\u002F","fixing broken layer paths"," for layers but not for paths stored as attribute values.",[194,464,466],{"id":465},"account-for-every-photo","Account for every photo",[14,468,469,470,473],{},"A survey that produced 412 photos and a layer with 389 points has 23 photos somewhere. The ",[230,471,472],{},"INVALID"," table lists them, but it is easy to write and never read. Compare counts straight away and report the gap.",[14,475,476],{},[29,477,480,483,486,489,492,498,506,509,515,522,528,533,536,540,544,546,549,552,555,559,562,565,572],{"viewBox":478,"role":32,"ariaLabel":479,"xmlns":34},"0 0 760 262","Reconciling photos in the folder with points on the map: 412 image files split into 389 placed points and 23 unplaced photos, of which most have no GPS tag and a few have a zero position",[36,481,482],{},"412 files in, where did they go?",[40,484,485],{},"A bar for 412 image files splits into 389 placed photos and 23 unplaced ones. The unplaced ones are broken down into 17 files with no GPS tags because location was off or the phone had no fix, 4 files edited by software that stripped EXIF, and 2 screenshots saved into the same folder. A separate warning notes that photos at exactly 0,0 are placed but wrong.",[44,487],{"x":46,"y":46,"width":47,"height":488,"fill":49},"262",[68,490,491],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Reconcile the folder with the layer every time",[68,493,497],{"x":494,"y":495,"style":496,"fill":73},"40","66","font-size:11px;font-weight:bold;font-family:sans-serif","412 files",[44,499],{"x":500,"y":501,"width":502,"height":503,"rx":504,"fill":505},"120","52","600","22","4","#d9d3c4",[44,507],{"x":500,"y":501,"width":508,"height":503,"rx":504,"fill":157},"566",[68,510,514],{"x":511,"y":512,"style":513,"fill":83,"textAnchor":74},"403","68","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","389 placed",[68,516,521],{"x":517,"y":518,"style":519,"fill":520,"textAnchor":74},"703","96","text-anchor:middle;font-size:10px;font-family:sans-serif","#b91c1c","23 not",[44,523],{"x":494,"y":524,"width":525,"height":526,"rx":82,"fill":527,"stroke":520,"style":85},"116","216","92","#fdf2e2",[68,529,532],{"x":175,"y":530,"style":531,"fill":520,"textAnchor":74},"146","text-anchor:middle;font-size:18px;font-weight:bold;font-family:sans-serif","17",[68,534,535],{"x":175,"y":146,"style":519,"fill":66,"textAnchor":74},"no GPS tag: location",[68,537,539],{"x":175,"y":538,"style":519,"fill":66,"textAnchor":74},"190","off or no satellite fix",[44,541],{"x":542,"y":524,"width":525,"height":526,"rx":82,"fill":527,"stroke":543,"style":85},"272","#b45309",[68,545,504],{"x":70,"y":530,"style":531,"fill":543,"textAnchor":74},[68,547,548],{"x":70,"y":146,"style":519,"fill":66,"textAnchor":74},"edited or resized and",[68,550,551],{"x":70,"y":538,"style":519,"fill":66,"textAnchor":74},"EXIF stripped on export",[44,553],{"x":554,"y":524,"width":525,"height":526,"rx":82,"fill":83,"stroke":84,"style":85},"504",[68,556,558],{"x":557,"y":530,"style":531,"fill":84,"textAnchor":74},"612","2",[68,560,561],{"x":557,"y":146,"style":519,"fill":66,"textAnchor":74},"screenshots saved",[68,563,564],{"x":557,"y":538,"style":519,"fill":66,"textAnchor":74},"into the survey folder",[44,566],{"x":500,"y":567,"width":568,"height":569,"rx":570,"fill":83,"stroke":520,"style":571},"222","520","30","6","stroke-width:1.5",[68,573,576],{"x":70,"y":574,"style":575,"fill":520,"textAnchor":74},"242","text-anchor:middle;font-size:10.5px;font-family:sans-serif","also check placed photos at exactly 0, 0 — tagged, but not located",[223,578,580],{"className":225,"code":579,"language":227,"meta":228,"style":228},"from pathlib import Path\nfrom qgis.core import QgsFeatureRequest\n\nimage_suffixes = {\".jpg\", \".jpeg\", \".tif\", \".tiff\"}\non_disk = {p.resolve() for p in Path(survey_day).rglob(\"*\")\n           if p.suffix.lower() in image_suffixes}\nplaced = {Path(f[\"photo\"]).resolve() for f in photos.getFeatures()}\n\nunplaced = sorted(on_disk - placed)\nprint(f\"{len(on_disk)} images, {len(placed)} placed, {len(unplaced)} unplaced\")\nfor p in unplaced:\n    print(\"  no position:\", p.name)\n\nnull_island = photos.getFeatures(QgsFeatureRequest().setFilterExpression(\n    \"abs(\\\"longitude\\\") \u003C 0.0001 AND abs(\\\"latitude\\\") \u003C 0.0001\"))\nprint(\"at 0,0:\", [f[\"filename\"] for f in null_island])\n",[230,581,582,594,605,609,640,667,680,706,710,729,773,784,797,801,811,840],{"__ignoreMap":228},[233,583,584,586,589,591],{"class":124,"line":235},[233,585,249],{"class":238},[233,587,588],{"class":242}," pathlib ",[233,590,239],{"class":238},[233,592,593],{"class":242}," Path\n",[233,595,596,598,600,602],{"class":124,"line":246},[233,597,249],{"class":238},[233,599,252],{"class":242},[233,601,239],{"class":238},[233,603,604],{"class":242}," QgsFeatureRequest\n",[233,606,607],{"class":124,"line":260},[233,608,264],{"emptyLinePlaceholder":263},[233,610,611,614,616,619,622,624,627,629,632,634,637],{"class":124,"line":267},[233,612,613],{"class":242},"image_suffixes ",[233,615,273],{"class":238},[233,617,618],{"class":242}," {",[233,620,621],{"class":276},"\".jpg\"",[233,623,409],{"class":242},[233,625,626],{"class":276},"\".jpeg\"",[233,628,409],{"class":242},[233,630,631],{"class":276},"\".tif\"",[233,633,409],{"class":242},[233,635,636],{"class":276},"\".tiff\"",[233,638,639],{"class":242},"}\n",[233,641,642,645,647,650,653,656,659,662,665],{"class":124,"line":280},[233,643,644],{"class":242},"on_disk ",[233,646,273],{"class":238},[233,648,649],{"class":242}," {p.resolve() ",[233,651,652],{"class":238},"for",[233,654,655],{"class":242}," p ",[233,657,658],{"class":238},"in",[233,660,661],{"class":242}," Path(survey_day).rglob(",[233,663,664],{"class":276},"\"*\"",[233,666,415],{"class":242},[233,668,669,672,675,677],{"class":124,"line":285},[233,670,671],{"class":238},"           if",[233,673,674],{"class":242}," p.suffix.lower() ",[233,676,658],{"class":238},[233,678,679],{"class":242}," image_suffixes}\n",[233,681,682,685,687,690,693,696,698,701,703],{"class":124,"line":302},[233,683,684],{"class":242},"placed ",[233,686,273],{"class":238},[233,688,689],{"class":242}," {Path(f[",[233,691,692],{"class":276},"\"photo\"",[233,694,695],{"class":242},"]).resolve() ",[233,697,652],{"class":238},[233,699,700],{"class":242}," f ",[233,702,658],{"class":238},[233,704,705],{"class":242}," photos.getFeatures()}\n",[233,707,708],{"class":124,"line":311},[233,709,264],{"emptyLinePlaceholder":263},[233,711,712,715,717,720,723,726],{"class":124,"line":327},[233,713,714],{"class":242},"unplaced ",[233,716,273],{"class":238},[233,718,719],{"class":320}," sorted",[233,721,722],{"class":242},"(on_disk ",[233,724,725],{"class":238},"-",[233,727,728],{"class":242}," placed)\n",[233,730,731,733,736,738,740,743,746,748,751,753,756,758,761,763,766,768,771],{"class":124,"line":355},[233,732,421],{"class":320},[233,734,735],{"class":242},"(",[233,737,335],{"class":238},[233,739,338],{"class":276},[233,741,742],{"class":320},"{len",[233,744,745],{"class":242},"(on_disk)",[233,747,347],{"class":320},[233,749,750],{"class":276}," images, ",[233,752,742],{"class":320},[233,754,755],{"class":242},"(placed)",[233,757,347],{"class":320},[233,759,760],{"class":276}," placed, ",[233,762,742],{"class":320},[233,764,765],{"class":242},"(unplaced)",[233,767,347],{"class":320},[233,769,770],{"class":276}," unplaced\"",[233,772,415],{"class":242},[233,774,775,777,779,781],{"class":124,"line":378},[233,776,652],{"class":238},[233,778,655],{"class":242},[233,780,658],{"class":238},[233,782,783],{"class":242}," unplaced:\n",[233,785,786,789,791,794],{"class":124,"line":384},[233,787,788],{"class":320},"    print",[233,790,735],{"class":242},[233,792,793],{"class":276},"\"  no position:\"",[233,795,796],{"class":242},", p.name)\n",[233,798,799],{"class":124,"line":389},[233,800,264],{"emptyLinePlaceholder":263},[233,802,803,806,808],{"class":124,"line":418},[233,804,805],{"class":242},"null_island ",[233,807,273],{"class":238},[233,809,810],{"class":242}," photos.getFeatures(QgsFeatureRequest().setFilterExpression(\n",[233,812,813,816,819,822,824,827,829,832,834,837],{"class":124,"line":432},[233,814,815],{"class":276},"    \"abs(",[233,817,818],{"class":320},"\\\"",[233,820,821],{"class":276},"longitude",[233,823,818],{"class":320},[233,825,826],{"class":276},") \u003C 0.0001 AND abs(",[233,828,818],{"class":320},[233,830,831],{"class":276},"latitude",[233,833,818],{"class":320},[233,835,836],{"class":276},") \u003C 0.0001\"",[233,838,839],{"class":242},"))\n",[233,841,843,845,847,850,853,856,859,861,863,865],{"class":124,"line":842},16,[233,844,421],{"class":320},[233,846,735],{"class":242},[233,848,849],{"class":276},"\"at 0,0:\"",[233,851,852],{"class":242},", [f[",[233,854,855],{"class":276},"\"filename\"",[233,857,858],{"class":242},"] ",[233,860,652],{"class":238},[233,862,700],{"class":242},[233,864,658],{"class":238},[233,866,867],{"class":242}," null_island])\n",[14,869,870,872,873,875,876,879],{},[205,871,440],{}," Building sets of resolved paths makes the comparison immune to trailing slashes and relative segments. The difference is the authoritative list of photos without a position, independent of whether the ",[230,874,472],{}," output was requested. The zero-position check catches a quieter failure: some devices write ",[230,877,878],{},"0, 0"," when they have no fix instead of omitting the tag, and those photos are placed — in the Gulf of Guinea. Remove them with a delete or a subset string before anyone zooms to the layer extent and wonders why it covers half the planet.",[194,881,883],{"id":882},"reproject-and-point-the-symbols-the-way-the-camera-faced","Reproject and point the symbols the way the camera faced",[14,885,886,887,890],{},"For analysis and for symbol rotation that looks right on a projected map, copy the points into your survey's CRS. Then use the ",[230,888,889],{},"direction"," field — the compass bearing the camera was facing — to rotate an arrow or a view-cone marker.",[223,892,894],{"className":225,"code":893,"language":227,"meta":228,"style":228},"from qgis.core import (\n    QgsProperty, QgsSymbolLayer, QgsMarkerSymbol, QgsSingleSymbolRenderer,\n)\n\nprojected = processing.run(\"native:reprojectlayer\", {\n    \"INPUT\": photos,\n    \"TARGET_CRS\": \"EPSG:27700\",\n    \"OUTPUT\": f\"{survey_day}\u002Fphotos_bng.gpkg\",\n})[\"OUTPUT\"]\nphotos_bng = QgsVectorLayer(projected, \"survey photos (BNG)\", \"ogr\")\n\nsymbol = QgsMarkerSymbol.createSimple({\n    \"name\": \"arrow\", \"size\": \"5\", \"color\": \"#b45309\",\n    \"outline_color\": \"#fffdf7\", \"outline_width\": \"0.3\",\n})\nsymbol.symbolLayer(0).setDataDefinedProperty(\n    QgsSymbolLayer.PropertyAngle,\n    QgsProperty.fromExpression('coalesce(\"direction\", 0)'),\n)\nphotos_bng.setRenderer(QgsSingleSymbolRenderer(symbol))\nQgsProject.instance().addMapLayer(photos_bng)\n",[230,895,896,907,912,916,920,934,942,954,975,985,1004,1008,1018,1050,1072,1076,1086,1092,1104,1109,1115],{"__ignoreMap":228},[233,897,898,900,902,904],{"class":124,"line":235},[233,899,249],{"class":238},[233,901,252],{"class":242},[233,903,239],{"class":238},[233,905,906],{"class":242}," (\n",[233,908,909],{"class":124,"line":246},[233,910,911],{"class":242},"    QgsProperty, QgsSymbolLayer, QgsMarkerSymbol, QgsSingleSymbolRenderer,\n",[233,913,914],{"class":124,"line":260},[233,915,415],{"class":242},[233,917,918],{"class":124,"line":267},[233,919,264],{"emptyLinePlaceholder":263},[233,921,922,925,927,929,932],{"class":124,"line":280},[233,923,924],{"class":242},"projected ",[233,926,273],{"class":238},[233,928,293],{"class":242},[233,930,931],{"class":276},"\"native:reprojectlayer\"",[233,933,299],{"class":242},[233,935,936,939],{"class":124,"line":285},[233,937,938],{"class":276},"    \"INPUT\"",[233,940,941],{"class":242},": photos,\n",[233,943,944,947,949,952],{"class":124,"line":302},[233,945,946],{"class":276},"    \"TARGET_CRS\"",[233,948,317],{"class":242},[233,950,951],{"class":276},"\"EPSG:27700\"",[233,953,324],{"class":242},[233,955,956,958,960,962,964,966,968,970,973],{"class":124,"line":311},[233,957,330],{"class":276},[233,959,317],{"class":242},[233,961,335],{"class":238},[233,963,338],{"class":276},[233,965,341],{"class":320},[233,967,344],{"class":242},[233,969,347],{"class":320},[233,971,972],{"class":276},"\u002Fphotos_bng.gpkg\"",[233,974,324],{"class":242},[233,976,977,980,982],{"class":124,"line":327},[233,978,979],{"class":242},"})[",[233,981,400],{"class":276},[233,983,984],{"class":242},"]\n",[233,986,987,990,992,995,998,1000,1002],{"class":124,"line":355},[233,988,989],{"class":242},"photos_bng ",[233,991,273],{"class":238},[233,993,994],{"class":242}," QgsVectorLayer(projected, ",[233,996,997],{"class":276},"\"survey photos (BNG)\"",[233,999,409],{"class":242},[233,1001,412],{"class":276},[233,1003,415],{"class":242},[233,1005,1006],{"class":124,"line":378},[233,1007,264],{"emptyLinePlaceholder":263},[233,1009,1010,1013,1015],{"class":124,"line":384},[233,1011,1012],{"class":242},"symbol ",[233,1014,273],{"class":238},[233,1016,1017],{"class":242}," QgsMarkerSymbol.createSimple({\n",[233,1019,1020,1023,1025,1028,1030,1033,1035,1038,1040,1043,1045,1048],{"class":124,"line":389},[233,1021,1022],{"class":276},"    \"name\"",[233,1024,317],{"class":242},[233,1026,1027],{"class":276},"\"arrow\"",[233,1029,409],{"class":242},[233,1031,1032],{"class":276},"\"size\"",[233,1034,317],{"class":242},[233,1036,1037],{"class":276},"\"5\"",[233,1039,409],{"class":242},[233,1041,1042],{"class":276},"\"color\"",[233,1044,317],{"class":242},[233,1046,1047],{"class":276},"\"#b45309\"",[233,1049,324],{"class":242},[233,1051,1052,1055,1057,1060,1062,1065,1067,1070],{"class":124,"line":418},[233,1053,1054],{"class":276},"    \"outline_color\"",[233,1056,317],{"class":242},[233,1058,1059],{"class":276},"\"#fffdf7\"",[233,1061,409],{"class":242},[233,1063,1064],{"class":276},"\"outline_width\"",[233,1066,317],{"class":242},[233,1068,1069],{"class":276},"\"0.3\"",[233,1071,324],{"class":242},[233,1073,1074],{"class":124,"line":432},[233,1075,381],{"class":242},[233,1077,1078,1081,1083],{"class":124,"line":842},[233,1079,1080],{"class":242},"symbol.symbolLayer(",[233,1082,46],{"class":320},[233,1084,1085],{"class":242},").setDataDefinedProperty(\n",[233,1087,1089],{"class":124,"line":1088},17,[233,1090,1091],{"class":242},"    QgsSymbolLayer.PropertyAngle,\n",[233,1093,1095,1098,1101],{"class":124,"line":1094},18,[233,1096,1097],{"class":242},"    QgsProperty.fromExpression(",[233,1099,1100],{"class":276},"'coalesce(\"direction\", 0)'",[233,1102,1103],{"class":242},"),\n",[233,1105,1107],{"class":124,"line":1106},19,[233,1108,415],{"class":242},[233,1110,1112],{"class":124,"line":1111},20,[233,1113,1114],{"class":242},"photos_bng.setRenderer(QgsSingleSymbolRenderer(symbol))\n",[233,1116,1118],{"class":124,"line":1117},21,[233,1119,1120],{"class":242},"QgsProject.instance().addMapLayer(photos_bng)\n",[14,1122,1123,1125,1126,1129],{},[205,1124,440],{}," EXIF direction is a bearing clockwise from north, which is also how QGIS marker rotation is measured, so no conversion is needed. ",[230,1127,1128],{},"coalesce"," supplies zero where the camera wrote no direction — many phones only record it with the compass calibrated — and it is worth styling those differently if direction matters to the survey. Marker rotation is applied relative to the map's north by default; on a projected CRS far from its central meridian, grid north and true north differ by a degree or two, which is below the accuracy of a phone compass anyway.",[14,1131,1132],{},[29,1133,1136,1139,1142,1145,1148,1153,1159,1166,1171,1176,1181,1185,1189,1193,1197,1203,1208,1212,1217,1221,1224],{"viewBox":1134,"role":32,"ariaLabel":1135,"xmlns":34},"0 0 760 250","Photo points drawn as arrows rotated to the camera direction field, with a map tip showing the photo when hovering a point",[36,1137,1138],{},"Direction as rotation, photo as map tip",[40,1140,1141],{},"Four arrow markers along a footpath each point the way their photo was taken: 212, 45, 300 and 130 degrees. One arrow has a map tip panel beside it showing a thumbnail placeholder, the filename and the capture time, driven by an HTML map tip template that reads the photo field.",[44,1143],{"x":46,"y":46,"width":47,"height":1144,"fill":49},"250",[68,1146,1147],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Readers see where the camera looked, then the photo",[44,1149],{"x":78,"y":1150,"width":1151,"height":1152,"rx":82,"fill":83,"stroke":84,"style":85},"46","420","186",[63,1154],{"d":1155,"fill":1156,"stroke":1157,"style":1158},"M50 200 Q150 120 240 150 T420 90","none","#9aa39d","stroke-width:6;stroke-dasharray:10 6",[1160,1161,1163],"g",{"transform":1162},"translate(96,168) rotate(212)",[63,1164],{"d":1165,"fill":543},"M0 -14 L7 6 L0 2 L-7 6 z",[1160,1167,1169],{"transform":1168},"translate(196,132) rotate(45)",[63,1170],{"d":1165,"fill":543},[1160,1172,1174],{"transform":1173},"translate(290,146) rotate(300)",[63,1175],{"d":1165,"fill":543},[1160,1177,1179],{"transform":1178},"translate(386,104) rotate(130)",[63,1180],{"d":1165,"fill":543},[68,1182,1184],{"x":518,"y":1183,"style":121,"fill":84,"textAnchor":74},"204","212°",[68,1186,1188],{"x":1187,"y":179,"style":121,"fill":84,"textAnchor":74},"196","45°",[68,1190,1192],{"x":1191,"y":108,"style":121,"fill":84,"textAnchor":74},"290","300°",[68,1194,1196],{"x":1195,"y":88,"style":121,"fill":84,"textAnchor":74},"386","130°",[44,1198],{"x":1199,"y":1200,"width":1201,"height":1202,"rx":82,"fill":83,"stroke":136,"style":137},"480","56","256","166",[44,1204],{"x":1205,"y":89,"width":525,"height":526,"rx":504,"fill":1206,"stroke":1157,"style":1207},"500","#e8e4d8","stroke-width:1",[63,1209],{"d":1210,"fill":1211},"M500 150 L560 110 L600 140 L640 104 L716 150 L716 166 L500 166 z","#c9d6cf",[1213,1214],"circle",{"cx":1215,"cy":518,"r":1216,"fill":505},"540","9",[68,1218,1220],{"x":1219,"y":538,"style":513,"fill":73,"textAnchor":74},"608","IMG_4172.jpg",[68,1222,1223],{"x":1219,"y":112,"style":519,"fill":66,"textAnchor":74},"14 Sep 2026, 10:31 · facing 212°",[124,1225],{"x1":1226,"y1":166,"x2":1227,"y2":1228,"stroke":136,"style":1229},"400","476","110","stroke-width:1.5;stroke-dasharray:4 3",[194,1231,1233],{"id":1232},"show-the-photo-in-the-map-tip","Show the photo in the map tip",[14,1235,1236],{},"Map tips accept HTML templates with expressions, so the stored path can become an image the moment somebody hovers a point.",[223,1238,1240],{"className":225,"code":1239,"language":227,"meta":228,"style":228},"from qgis.core import QgsMapLayer\n\ntemplate = (\n    '\u003Cimg src=\"file:\u002F\u002F\u002F[% replace(\"photo\", \\'\\\\\\\\\\', \\'\u002F\\') %]\" width=\"320\"\u002F>'\n    '\u003Cp>\u003Cb>[% \"filename\" %]\u003C\u002Fb>\u003Cbr\u002F>'\n    '[% format_date(\"timestamp\", \\'d MMM yyyy, HH:mm\\') %] · '\n    'facing [% coalesce(round(\"direction\"), \\'?\\') %]°\u003C\u002Fp>'\n)\nphotos_bng.setMapTipTemplate(template)\nphotos_bng.setDisplayExpression('\"filename\"')\n",[230,1241,1242,1253,1257,1266,1293,1298,1319,1340,1344,1349],{"__ignoreMap":228},[233,1243,1244,1246,1248,1250],{"class":124,"line":235},[233,1245,249],{"class":238},[233,1247,252],{"class":242},[233,1249,239],{"class":238},[233,1251,1252],{"class":242}," QgsMapLayer\n",[233,1254,1255],{"class":124,"line":246},[233,1256,264],{"emptyLinePlaceholder":263},[233,1258,1259,1262,1264],{"class":124,"line":260},[233,1260,1261],{"class":242},"template ",[233,1263,273],{"class":238},[233,1265,906],{"class":242},[233,1267,1268,1271,1274,1277,1280,1282,1285,1288,1290],{"class":124,"line":267},[233,1269,1270],{"class":276},"    '\u003Cimg src=\"file:\u002F\u002F\u002F[",[233,1272,1273],{"class":320},"% r",[233,1275,1276],{"class":276},"eplace(\"photo\", ",[233,1278,1279],{"class":320},"\\'\\\\\\\\\\'",[233,1281,409],{"class":276},[233,1283,1284],{"class":320},"\\'",[233,1286,1287],{"class":276},"\u002F",[233,1289,1284],{"class":320},[233,1291,1292],{"class":276},") %]\" width=\"320\"\u002F>'\n",[233,1294,1295],{"class":124,"line":280},[233,1296,1297],{"class":276},"    '\u003Cp>\u003Cb>[% \"filename\" %]\u003C\u002Fb>\u003Cbr\u002F>'\n",[233,1299,1300,1303,1306,1309,1311,1314,1316],{"class":124,"line":285},[233,1301,1302],{"class":276},"    '[",[233,1304,1305],{"class":320},"% f",[233,1307,1308],{"class":276},"ormat_date(\"timestamp\", ",[233,1310,1284],{"class":320},[233,1312,1313],{"class":276},"d MMM yyyy, HH:mm",[233,1315,1284],{"class":320},[233,1317,1318],{"class":276},") %] · '\n",[233,1320,1321,1324,1327,1330,1332,1335,1337],{"class":124,"line":302},[233,1322,1323],{"class":276},"    'facing [",[233,1325,1326],{"class":320},"% c",[233,1328,1329],{"class":276},"oalesce(round(\"direction\"), ",[233,1331,1284],{"class":320},[233,1333,1334],{"class":276},"?",[233,1336,1284],{"class":320},[233,1338,1339],{"class":276},") %]°\u003C\u002Fp>'\n",[233,1341,1342],{"class":124,"line":311},[233,1343,415],{"class":242},[233,1345,1346],{"class":124,"line":327},[233,1347,1348],{"class":242},"photos_bng.setMapTipTemplate(template)\n",[233,1350,1351,1354,1357],{"class":124,"line":355},[233,1352,1353],{"class":242},"photos_bng.setDisplayExpression(",[233,1355,1356],{"class":276},"'\"filename\"'",[233,1358,415],{"class":242},[14,1360,1361,1363,1364,1367,1368,1371,1372,1376],{},[205,1362,440],{}," Expressions inside ",[230,1365,1366],{},"[% %]"," are evaluated per feature. Swapping backslashes for forward slashes keeps the ",[230,1369,1370],{},"file:\u002F\u002F\u002F"," URL valid for Windows paths. The width attribute keeps a 12-megapixel photo from filling the screen. Map tips must also be switched on in the QGIS toolbar; for a record-by-record review, the same image can instead go into an ",[21,1373,1375],{"href":1374},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fconfigure-editor-widgets-pyqgis\u002F","attribute form"," with an attachment widget, which also lets reviewers open the full-size file.",[194,1378,1380],{"id":1379},"split-a-multi-day-survey-by-time","Split a multi-day survey by time",[14,1382,1383,1384,1387],{},"When photos from several days or several crews land in one folder, the ",[230,1385,1386],{},"timestamp"," field is the cleanest way to separate them — more reliable than folder names, which crews invent on the spot. Camera clocks are a hazard, though: a phone set to the wrong time zone shifts every photo by hours, and a camera whose battery was changed may think it is 2000.",[223,1389,1391],{"className":225,"code":1390,"language":227,"meta":228,"style":228},"from qgis.core import QgsExpression, QgsFeatureRequest\n\ndays = photos.uniqueValues(photos.fields().indexOf(\"timestamp\"))\nby_day = sorted({v.date().toString(\"yyyy-MM-dd\") for v in days if v})\nprint(\"capture days:\", by_day)\n\nsuspicious = QgsFeatureRequest().setFilterExpression(\n    \"year(\\\"timestamp\\\") \u003C 2020 OR \\\"timestamp\\\" > now()\")\nprint(\"clock problems:\", [f[\"filename\"] for f in photos.getFeatures(suspicious)])\n\nfor day in by_day:\n    expr = f\"format_date(\\\"timestamp\\\", 'yyyy-MM-dd') = {QgsExpression.quotedValue(day)}\"\n    processing.run(\"native:extractbyexpression\", {\n        \"INPUT\": photos,\n        \"EXPRESSION\": expr,\n        \"OUTPUT\": f\"{survey_day}\u002Fphotos_{day}.gpkg\",\n    })\n",[230,1392,1393,1404,1408,1423,1457,1469,1473,1483,1508,1532,1536,1548,1580,1590,1597,1605,1637],{"__ignoreMap":228},[233,1394,1395,1397,1399,1401],{"class":124,"line":235},[233,1396,249],{"class":238},[233,1398,252],{"class":242},[233,1400,239],{"class":238},[233,1402,1403],{"class":242}," QgsExpression, QgsFeatureRequest\n",[233,1405,1406],{"class":124,"line":246},[233,1407,264],{"emptyLinePlaceholder":263},[233,1409,1410,1413,1415,1418,1421],{"class":124,"line":260},[233,1411,1412],{"class":242},"days ",[233,1414,273],{"class":238},[233,1416,1417],{"class":242}," photos.uniqueValues(photos.fields().indexOf(",[233,1419,1420],{"class":276},"\"timestamp\"",[233,1422,839],{"class":242},[233,1424,1425,1428,1430,1432,1435,1438,1441,1443,1446,1448,1451,1454],{"class":124,"line":267},[233,1426,1427],{"class":242},"by_day ",[233,1429,273],{"class":238},[233,1431,719],{"class":320},[233,1433,1434],{"class":242},"({v.date().toString(",[233,1436,1437],{"class":276},"\"yyyy-MM-dd\"",[233,1439,1440],{"class":242},") ",[233,1442,652],{"class":238},[233,1444,1445],{"class":242}," v ",[233,1447,658],{"class":238},[233,1449,1450],{"class":242}," days ",[233,1452,1453],{"class":238},"if",[233,1455,1456],{"class":242}," v})\n",[233,1458,1459,1461,1463,1466],{"class":124,"line":280},[233,1460,421],{"class":320},[233,1462,735],{"class":242},[233,1464,1465],{"class":276},"\"capture days:\"",[233,1467,1468],{"class":242},", by_day)\n",[233,1470,1471],{"class":124,"line":285},[233,1472,264],{"emptyLinePlaceholder":263},[233,1474,1475,1478,1480],{"class":124,"line":302},[233,1476,1477],{"class":242},"suspicious ",[233,1479,273],{"class":238},[233,1481,1482],{"class":242}," QgsFeatureRequest().setFilterExpression(\n",[233,1484,1485,1488,1490,1492,1494,1497,1499,1501,1503,1506],{"class":124,"line":311},[233,1486,1487],{"class":276},"    \"year(",[233,1489,818],{"class":320},[233,1491,1386],{"class":276},[233,1493,818],{"class":320},[233,1495,1496],{"class":276},") \u003C 2020 OR ",[233,1498,818],{"class":320},[233,1500,1386],{"class":276},[233,1502,818],{"class":320},[233,1504,1505],{"class":276}," > now()\"",[233,1507,415],{"class":242},[233,1509,1510,1512,1514,1517,1519,1521,1523,1525,1527,1529],{"class":124,"line":327},[233,1511,421],{"class":320},[233,1513,735],{"class":242},[233,1515,1516],{"class":276},"\"clock problems:\"",[233,1518,852],{"class":242},[233,1520,855],{"class":276},[233,1522,858],{"class":242},[233,1524,652],{"class":238},[233,1526,700],{"class":242},[233,1528,658],{"class":238},[233,1530,1531],{"class":242}," photos.getFeatures(suspicious)])\n",[233,1533,1534],{"class":124,"line":355},[233,1535,264],{"emptyLinePlaceholder":263},[233,1537,1538,1540,1543,1545],{"class":124,"line":378},[233,1539,652],{"class":238},[233,1541,1542],{"class":242}," day ",[233,1544,658],{"class":238},[233,1546,1547],{"class":242}," by_day:\n",[233,1549,1550,1553,1555,1558,1561,1563,1565,1567,1570,1572,1575,1577],{"class":124,"line":384},[233,1551,1552],{"class":242},"    expr ",[233,1554,273],{"class":238},[233,1556,1557],{"class":238}," f",[233,1559,1560],{"class":276},"\"format_date(",[233,1562,818],{"class":320},[233,1564,1386],{"class":276},[233,1566,818],{"class":320},[233,1568,1569],{"class":276},", 'yyyy-MM-dd') = ",[233,1571,341],{"class":320},[233,1573,1574],{"class":242},"QgsExpression.quotedValue(day)",[233,1576,347],{"class":320},[233,1578,1579],{"class":276},"\"\n",[233,1581,1582,1585,1588],{"class":124,"line":389},[233,1583,1584],{"class":242},"    processing.run(",[233,1586,1587],{"class":276},"\"native:extractbyexpression\"",[233,1589,299],{"class":242},[233,1591,1592,1595],{"class":124,"line":418},[233,1593,1594],{"class":276},"        \"INPUT\"",[233,1596,941],{"class":242},[233,1598,1599,1602],{"class":124,"line":432},[233,1600,1601],{"class":276},"        \"EXPRESSION\"",[233,1603,1604],{"class":242},": expr,\n",[233,1606,1607,1610,1612,1614,1616,1618,1620,1622,1625,1627,1630,1632,1635],{"class":124,"line":842},[233,1608,1609],{"class":276},"        \"OUTPUT\"",[233,1611,317],{"class":242},[233,1613,335],{"class":238},[233,1615,338],{"class":276},[233,1617,341],{"class":320},[233,1619,344],{"class":242},[233,1621,347],{"class":320},[233,1623,1624],{"class":276},"\u002Fphotos_",[233,1626,341],{"class":320},[233,1628,1629],{"class":242},"day",[233,1631,347],{"class":320},[233,1633,1634],{"class":276},".gpkg\"",[233,1636,324],{"class":242},[233,1638,1639],{"class":124,"line":1088},[233,1640,1641],{"class":242},"    })\n",[14,1643,1644,441,1646,1649,1650,1653,1654,1657,1658,1662],{},[205,1645,440],{},[230,1647,1648],{},"uniqueValues"," returns the distinct timestamps as ",[230,1651,1652],{},"QDateTime"," objects, so reducing them to dates gives the list of survey days without iterating every feature in Python. The suspicious-clock filter catches the two common failures — a reset clock and a future date from a wrong time zone — before they create a phantom survey day. ",[230,1655,1656],{},"QgsExpression.quotedValue"," produces a correctly quoted literal, which avoids hand-building quotes inside the expression. Writing one GeoPackage per day suits a reporting rhythm where each day's photos are reviewed and signed off separately; for a single project with a day filter, a ",[21,1659,1661],{"href":1660},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fcreate-map-theme-pyqgis\u002F","map theme"," or a subset string on one layer is lighter.",[194,1664,1666],{"id":1665},"qgis-version-compatibility","QGIS version compatibility",[14,1668,1669,1672,1673,1676,1677,1680],{},[230,1670,1671],{},"native:importphotos"," has been available since QGIS 3.6 with the same parameters and output fields. Map tip templates are 3.0+. ",[230,1674,1675],{},"QgsSymbolLayer.PropertyAngle"," is spelled ",[230,1678,1679],{},"QgsSymbolLayer.Property.Angle"," from 3.30 onward and only in that scoped form on the QGIS 4 series. Support for HEIC images depends on the GDAL build; convert them to JPEG before import if the algorithm skips them.",[194,1682,1684],{"id":1683},"troubleshooting","Troubleshooting",[199,1686,1687,1693,1702,1710,1716],{},[202,1688,1689,1692],{},[205,1690,1691],{},"No points at all."," The folder holds HEIC or PNG images, or location tagging was off. Check one file with an EXIF viewer.",[202,1694,1695,1698,1699,1701],{},[205,1696,1697],{},"Every point has altitude 0."," The camera did not record ",[230,1700,452],{},", or the photos were exported from a gallery app that dropped it.",[202,1703,1704,441,1707,1709],{},[205,1705,1706],{},"Arrows all point north.",[230,1708,889],{}," is null for every photo — the phone did not record it.",[202,1711,1712,1715],{},[205,1713,1714],{},"Map tip shows a broken image."," The path moved, or it contains characters that need URL encoding; test with a simple path first.",[202,1717,1718,1721],{},[205,1719,1720],{},"Photos appear offset by a few metres."," Phone GPS accuracy in urban canyons is often 5–15 m; that is the data, not the import.",[194,1723,1725],{"id":1724},"conclusion","Conclusion",[14,1727,1728,1729,1731,1732,1734,1735,1737],{},"Run ",[230,1730,1671],{}," with an ",[230,1733,472],{}," output, then reconcile the folder against the layer yourself so no photo goes missing unnoticed, and filter out positions at 0, 0. Reproject for analysis, rotate markers by the ",[230,1736,889],{}," field, and put the image in a map tip so the layer answers \"what did it look like here?\" without leaving QGIS.",[194,1739,1741],{"id":1740},"frequently-asked-questions","Frequently Asked Questions",[14,1743,1744,1747],{},[205,1745,1746],{},"Can I import photos that have no GPS tags if I have a GPS track?","\nNot with this algorithm. Match photo timestamps to track points by time — a nearest-time join — and write the positions back.",[14,1749,1750,1753],{},[205,1751,1752],{},"Does the import copy the images?","\nNo. It stores paths to the original files, so keep the folder where it is or store relative paths.",[14,1755,1756,1759],{},[205,1757,1758],{},"Can I import from a phone directly?","\nOnly from a folder QGIS can read; copy the photos to disk first.",[14,1761,1762,1765,1766,1768],{},[205,1763,1764],{},"How do I include the images in a printed report?","\nUse a picture item in a layout, with its source set by an expression on the ",[230,1767,456],{}," field, and drive it from an atlas over the photo layer.",[194,1770,1772],{"id":1771},"related","Related",[199,1774,1775,1780,1786,1792,1798],{},[202,1776,1777,1779],{},[21,1778,24],{"href":23}," — the guide this recipe belongs to",[202,1781,1782],{},[21,1783,1785],{"href":1784},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fload-csv-as-point-layer-pyqgis\u002F","Load a CSV as a Point Layer in PyQGIS",[202,1787,1788],{},[21,1789,1791],{"href":1790},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fdata-defined-symbol-size-expression-pyqgis\u002F","Data-Defined Symbol Size with an Expression in PyQGIS",[202,1793,1794],{},[21,1795,1797],{"href":1796},"\u002Fspatial-data-processing-automation\u002Fautomating-atlas-map-series\u002Fconfigure-atlas-coverage-layer-pyqgis\u002F","Configure an Atlas Coverage Layer in PyQGIS",[202,1799,1800],{},[21,1801,1803],{"href":1802},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002F","Transform Point Coordinates Between CRS in PyQGIS",[1805,1806,1807],"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);}",{"title":228,"searchDepth":246,"depth":246,"links":1809},[1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820],{"id":196,"depth":246,"text":197},{"id":217,"depth":246,"text":218},{"id":465,"depth":246,"text":466},{"id":882,"depth":246,"text":883},{"id":1232,"depth":246,"text":1233},{"id":1379,"depth":246,"text":1380},{"id":1665,"depth":246,"text":1666},{"id":1683,"depth":246,"text":1684},{"id":1724,"depth":246,"text":1725},{"id":1740,"depth":246,"text":1741},{"id":1771,"depth":246,"text":1772},"Turn a folder of field photos into a point layer with native:importphotos — reading EXIF position, direction and timestamp, catching photos with no GPS fix, rotating symbols to camera bearing and showing the image in map tips.","md",{"slug":1824,"type":1825,"breadcrumb":1826,"datePublished":1827,"dateModified":1827},"import-geotagged-photos-as-points-pyqgis","article","Import Geotagged Photos as Points","2026-09-17","\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fimport-geotagged-photos-as-points-pyqgis",{"title":5,"description":1821},"spatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fimport-geotagged-photos-as-points-pyqgis\u002Findex","fugRFzLJMEL99sgDvvBhjKz064MlpG2y72HRiaBsKSg",1789632908442]