Using AutoLISP / Visual Lisp to Geo-Locate Geotagged Images

Using AutoLISP / Visual Lisp to Geo-Locate Geotagged Images

CodeDing
Advisor Advisor
6,075 Views
23 Replies
Message 1 of 24

Using AutoLISP / Visual Lisp to Geo-Locate Geotagged Images

CodeDing
Advisor
Advisor

Hello all,

 

I had to come up with a unique solution recently and I feel that it may benefit others here. I have attached the Lisp "PHOTOS.lsp" (that is also the command name).

 

The lisp accomplishes 1 task: Locate any Geotagged photos (from user selected folder [and all sub-folders]) and show their locations in a Geo-Located dwg via a block you choose, which gets hyperlinked to the image.

 

When you open the lisp, you will need to update the "Variable Inputs" section at the beginning of the PHOTOS command (toward bottom). Here's a preview of that section:

  ;; Variable Inputs
  (setq blkName "MyBlockName"        ;<-- the block to insert for all photo locations (do NOT use dynamic block)
        blkDWG "C:\\users\\my folder\\myblockdrawing.dwg" ;<-- if block not in current dwg, where to find it
        lyrName "_Geo-Photos"        ;<-- layer where blocks will be inserted
        lyrColor 11                  ;<-- if necessary to create layer
        defaultBrowseLocation "C:\\" ;<-- the default path that LM's folder browser opens to
  );setq

 

Here's some screenshots of the workflow:

Run command

CodeDing_0-1618518608529.png

 

Browse folders dialog will appear, select the folder which you want to search for all photos

image.png

 

The command begins, once complete you will be prompted:

CodeDing_1-1618518801744.png

 

Here is an example of what my final product looks like.

CodeDing_2-1618518819947.png

(no, I won't be sharing my camera block)

 

 

SOME IMPORTANT NOTES:

- Clearly this only works if your drawing is Geo-Located (any vertical)

- Most images are taken from phones, and their GPS accuracy can vary, so the location is NOT 100% reliable. I think on average it's about a 40 foot radius of accuracy.

- Sometimes a phone coordinate can be the same for multiple photos, so you might see multiple stacked above each other.

- Yes, hyperlinks are created and when you Ctrl + Click the application is Minimized... AutoCAD knows about this 'feature' and has never fixed it (it's probably not an easy fix, idk).

- Not all images are Geotagged, so if you are noticing that not all photos are being captured, it's most likely because they were not Geotagged.

 

If you have any questions, please ask.

Best,

~DD

 

Accepted solutions (1)
6,076 Views
23 Replies
Replies (23)
Message 2 of 24

ronjonp
Advisor
Advisor

@CodeDing 

Looks pretty cool, nice work! 🍻

Message 3 of 24

Anonymous
Not applicable

Did this a few years ago also using exiftool that read the lat long out of the photo, so made a xyz file lat,long, photo name, as CIV3D has lat long input just loaded the actual photos over the top of the aerial image so got a plan and a eye view. Found the accuracy a bit off was about 6m varied all over the place, photo was taken by a normal phone camera so not bad.

 

exiftool -filename -gpslatitude -gpslongitude -T "p:\2015 projects\2015104\photos" > out.txt

Message 4 of 24

ВeekeeCZ
Consultant
Consultant

Sometimes it's useful. We use JOSM HERE for it. It looks like this.

Z9E3zK5E_0-1618559865439.png

 

In the past we used some gps tracker (garmin tourist sat-nav) and digital camera, parred them by time. Now it's easier with cellphones, but it's necessary to keep the GPS module busy to keep accuracy consistent - I typically use Strava-walk activity.

 

Anyway, just last week we (with @vladimir_michl ) played with another cool feature using geolocation, see HERE . Just specify a vector for the base and direction, and voila... 

The main conversion func from Vladimir is HERE (I didn't test another coord system than ours), then simple lisp to concatenate a link: 

 

(defun c:Mapsview ( / jtsk osm 2nd ang gps url)
  
  (and (or geo2gps (load "Geo2Gps.vlx") geo2gps)
       (setq osm (getvar 'osmode))
       (setvar 'osmode 0)
       (setq jtsk (getpoint "\nSpecify base point for streetview: "))
       (setq 2nd (getpoint jtsk "\nSpecify a direction: "))
       (setvar 'osmode osm)
       (setq ang (angle (trans jtsk 1 0) (trans 2nd 1 0)))
       (setq gps (geo2gps (trans jtsk 1 0)))
       (setvar 'cmdecho 0)
       (vl-cmdf "_.browser" (setq url (strcat "https://www.google.com/maps/@?api=1&map_action=pano&viewpoint="
					      (vl-princ-to-string (car gps)) "," (vl-princ-to-string (cadr gps))
					      "&heading=" (angtos (- (/ pi 2) ang) 0 0) "&pitch=0&fov=80")))
       (setvar 'cmdecho 1)
       (princ (strcat "\n>>> " url)))
  (princ)
  )

 

Or possibly another...

 

       (vl-cmdf "_.browser" (setq url (strcat "https://www.google.com/maps/@"
					      (vl-princ-to-string (car gps)) "," (vl-princ-to-string (cadr gps))
					      ",100m/data=!3m1!1e3"))) 

 

Message 5 of 24

JanardFugro
Contributor
Contributor

Hi  CodeDing,

 

I tried this lisp. But it's not working for some reasons. I didn't understand what's wrong.   Our template is georeferenced, and photos are with geotags, block is present in the drawing everything is there, but not importing the photos. Some helpful comments please.

0 Likes
Message 6 of 24

s_wait3TDER
Explorer
Explorer

Nice lisp! It does exactly what I want except for the coordinate system.  I've tried to modify your lisp and convert the LL coordinates in Canada nad 83 mtm 8 (CANQ-M8) with ade_projptforward command but failed.  Can  you help me?

0 Likes
Message 7 of 24

CodeDing
Advisor
Advisor
Accepted solution

Hey All,

 

Just an update, I create a PHOTOPREVIEW command on another post to compliment this tool:

https://forums.autodesk.com/t5/visual-lisp-autolisp-and-general/javascript-api-add-images-to-tool-pa... 

 

@JanardFugro & @s_wait3TDER ,

Did you guys get your issues resolved? I didn't realize there were updates to this post. If you're still around and need help please let me know.

 

Best,

~DD

Message 8 of 24

JanardFugro
Contributor
Contributor

Hi,

 

I didn't understand, what is the issue in the code. I have the block in my template and  given my path for the block drawing as well. No block inserted and no error shown while running the command.  

0 Likes
Message 9 of 24

MartijnDC
Observer
Observer

Hi @CodeDing 

 

I was wondering if this code can get edited so it does NOT include subfolders?

 

Changing 

(vl-remove ".." (vl-remove "." (vl-directory-files fldr nil -1))) 

to 

(vl-remove ".." (vl-remove "." (vl-directory-files fldr nil 1)))

seems to do the trick, but is that the right way?

 

 

 

;; Returns full links for all jpg, png, and bmp photos inside provided folder and all subfolders.
(defun PHOTOS_GetPhotoLinks (folder / GetLinks)
  (setq GetLinks
    (lambda (fldr patt)
      (apply
        'append
        (cons
          (mapcar
            '(lambda (f) (strcat fldr "\\" f))
            (vl-directory-files fldr patt)
          );mapcar
          (mapcar
            '(lambda (f) (GetLinks (strcat fldr "\\" f) patt))
            (vl-remove ".." (vl-remove "." (vl-directory-files fldr nil -1)))
          );mapcar
        );cons
      );apply
    );lambda
  );setq
  (apply
    'append
    (mapcar
      '(lambda (str) (GetLinks folder str))
      '("*.jpg" "*.png" "*.bmp")
    );mapcar
  );apply
);defun

 

 

 

 

Message 10 of 24

CodeDing
Advisor
Advisor

@MartijnDC ,

 

The fix you found would work. Here is a little shorter version:

;; Returns full links for all jpg, png, and bmp photos inside provided folder (no subfolders).
(defun PHOTOS_GetPhotoLinks (folder / GetLinks)
  (setq GetLinks
    (lambda (fldr patt)
      (mapcar
       '(lambda (f) (strcat fldr "\\" f))
        (vl-directory-files fldr patt 1)
      );mapcar
    );lambda
  );setq
  (apply
    'append
    (mapcar
      '(lambda (str) (GetLinks folder str))
      '("*.jpg" "*.png" "*.bmp")
    );mapcar
  );apply
);defun

 

Best,

~DD

Message 11 of 24

Sea-Haven
Mentor
Mentor

Just a comment years ago did insert phone photos, it is easy if you have CIV3D, you can import using lat and long. The value of the lat and long was read from the jpg image using a external program. I think it was Exiftool.exe, so just made a csv file with PENZD the desription was image name, so matched that to a RASTERINSERT and placed the image at correct location. I will try to find the code.

 

This is part 1 will try to find part 2.

 

; import mobile phone phot0s 
; use exiftool to make pnts file with lat long
; By AlanH 

(defun c:phonephotos ( / *acad* c3ddoc pnts ptopts filename pnts)
(setq *acad* (vlax-get-acad-object))

  (setq   C3D (strcat "HKEY_LOCAL_MACHINE\\"
                                    (if        vlax-user-product-key
                                      (vlax-user-product-key)
                                      (vlax-product-key)
                                    )
                    )
                C3D (vl-registry-read C3D "Release")
                C3D (substr
                      C3D
                      1
                      (vl-string-search "." C3D (+ (vl-string-search "." C3D) 1))
                    )
                C3Dapp (vla-getinterfaceobject
                      *acad*
                      (strcat "AeccXUiLand.AeccApplication." C3D)
                    )
  )

  (setq C3Ddoc (vla-get-activedocument C3Dapp))

  (setq pnts (vlax-get C3Ddoc 'points))

  (setq ptopts (vla-getinterfaceobject *acad* (strcat "AeccXLand.AeccPointImportOptions." C3D)))

  (setq filename "C:\\acadtemp\\exiftool\\out.lat")

  (setq pnts (vlax-invoke pnts 'importpoints filename "LATLONG" ptopts))

  
  (vlax-release-object ptopts)
  (vlax-release-object c3Dapp)
)

Found some more code.

exiftool -filename -gpslatitude -gpslongitude -T "p:\2015 projects\2015104\photos" > out.txt

 

0 Likes
Message 12 of 24

engsurveymohamed
Observer
Observer

Hi,

When I use Lisp, I get the message " unable to locate drawing. 

any Help please  

0 Likes
Message 13 of 24

CodeDing
Advisor
Advisor

@engsurveymohamed ,

 

Can you post your Variable Inputs please?

 

Best,

~DD

0 Likes
Message 14 of 24

JanardFugro
Contributor
Contributor

Hi  CodeDing,

 

My issue is not solved yet. What I am doing now is, importing photos into ArcGIS, creating point shape file, mapimport that .shp points. then inserting photo markers with my own lisp program.

 

Thank you.

0 Likes
Message 15 of 24

CodeDing
Advisor
Advisor

@JanardFugro ,

  1. Please post your Variable Inputs
  2. Your block
  3. A GeoLocated DWG file
0 Likes
Message 16 of 24

JanardFugro
Contributor
Contributor

I have used the same lisp, only changed the block name area. Please have a look and correct me, that will be appreciated. Thank you.

 

;; Retrieves EXIF data from specified image file
(defun ExifData (file / err idata iprop oimg)
  (if (and (setq file (findfile file))
           (setq oimg (vlax-create-object "WIA.Imagefile"))
      );and
    (progn
      (setq err
        (vl-catch-all-apply
          (function
            (lambda nil
              (vlax-invoke-method oimg 'loadfile file)
              (setq iprop (vlax-get-property oimg 'properties))
              (vlax-for x iprop
                (setq idata
                  (cons
                    (cons
                      (vlax-get-property x 'name)
                      (vlax-variant-value (vlax-get-property x 'value))
                    );cons
                    idata
                  );cons
                );setq
              );vlax-for
            );lambda
          );function
        );vl-catch
      );setq
      (foreach obj (list iprop oimg)
        (if (= 'vla-object (type obj))
          (vlax-release-object obj)
        );if
      );foreach
      (if (null (vl-catch-all-error-p err)) (reverse idata))
    );progn
  );if
);defun
 
;; Browse for Folder  -  Lee Mac
;; Displays a dialog prompting the user to select a folder.
;; msg - [str] message to display at top of dialog
;; dir - [str] [optional] root directory (or nil)
;; bit - [int] bit-coded flag specifying dialog display settings
;; Returns: [str] Selected folder filepath, else nil.
(defun LM:browseforfolder ( msg dir bit / err fld pth shl slf )
    (setq err
        (vl-catch-all-apply
            (function
                (lambda ( / app hwd )
                    (if (setq app (vlax-get-acad-object)
                              shl (vla-getinterfaceobject app "shell.application")
                              hwd (vl-catch-all-apply 'vla-get-hwnd (list app))
                              fld (vlax-invoke-method shl 'browseforfolder (if (vl-catch-all-error-p hwd) 0 hwd) msg bit dir)
                        )
                        (setq slf (vlax-get-property fld 'self)
                              pth (vlax-get-property slf 'path)
                              pth (vl-string-right-trim "\\" (vl-string-translate "/" "\\" pth))
                        )
                    )
                )
            )
        )
    )
    (if slf (vlax-release-object slf))
    (if fld (vlax-release-object fld))
    (if shl (vlax-release-object shl))
    (if (vl-catch-all-error-p err)
        (prompt (vl-catch-all-error-message err))
        pth
    )
)
 
;; Entmake's an arbitrary GeoMarker
;; returns - ename, of marker
(defun GeoMarker ( / )
  (entmakex '((0 . "POSITIONMARKER") (100 . "AcDbEntity") (100 . "AcDbGeoPositionMarker") (90 . 0) (10 0.0 0.0 0.0) (40 . 1.0)
             (1 . "") (40 . 0.5) (290 . 0) (280 . 0) (290 . 1) (101 . "Embedded Object") (100 . "AcDbEntity") (100 . "AcDbMText")
             (10 0.1 0.1 0.0) (40 . 1.0) (1 . "") (210 0.0 0.0 1.0) (11 1.0 0.0 0.0) (42 . 9761.9) (43 . 6666.67)))
);defun
 
;; Turns a (long lat) into coordinates ..ONLY useable if dwg is Geo-Located.
;; pt - point as (long lat), 
;; returns - point, as (X Y Z[0.0]) ...since longitudes represent "x" values & Latitudes represent "y" values
(defun LL->PT (LL / e ev return)
  (if (and LL
           (setq e (GeoMarker)))
    (progn
      (setq ev (vlax-ename->vla-object e))
      (vlax-put-property ev 'Longitude (rtos (car LL) 2 7))
      (vlax-put-property ev 'Latitude (rtos (cadr LL) 2 7))
      (setq return
        (list
          (getpropertyvalue e "Position/X")
          (getpropertyvalue e "Position/Y")
          0.0
        );list
      );setq
      (entdel e)
      return
    );progn
  );if
);defun
 
;; Returns full links for all jpg, png, and bmp photos inside provided folder and all subfolders.
(defun PHOTOS_GetPhotoLinks (folder / GetLinks)
  (setq GetLinks
    (lambda (fldr patt)
      (apply
        'append
        (cons
          (mapcar
            '(lambda (f) (strcat fldr "\\" f))
            (vl-directory-files fldr patt)
          );mapcar
          (mapcar
            '(lambda (f) (GetLinks (strcat fldr "\\" f) patt))
            (vl-remove ".." (vl-remove "." (vl-directory-files fldr nil -1)))
          );mapcar
        );cons
      );apply
    );lambda
  );setq
  (apply
    'append
    (mapcar
      '(lambda (str) (GetLinks folder str))
      '("*.jpg" "*.png" "*.bmp")
    );mapcar
  );apply
);defun
 
;; Converts Vector from EXIF data to Decimal degrees.
(defun PHOTOS_Vec2Dec (vec / v dec m)
  (vlax-for v vec
    (setq v (vlax-get v 'Value))
    (cond
      ((and dec m) (setq dec (+ dec (/ v 3600))))
      (dec (setq dec (+ dec (setq m (/ v 60)))))
      (t (setq dec v))
    );cond
  );vlax-for
);defun
 
;; If an image file has GPS data, return list of file & lon/lat
(defun PHOTOS_GetGeoData (file / data lon lat lonDir latDir)
  (if (and (setq data (ExifData file))
           (setq lon (cdr (assoc "GpsLongitude" data)))
           (setq lat (cdr (assoc "GpsLatitude" data)))
           (setq lonDir (cdr (assoc "GpsLongitudeRef" data)))
           (setq latDir (cdr (assoc "GpsLatitudeRef" data)))
           (setq lonDir (if (eq "E" (strcase lonDir)) + -))
           (setq latDir (if (eq "N" (strcase latDir)) + -))
           (setq lon (lonDir (PHOTOS_Vec2Dec lon)))
           (setq lat (latDir (PHOTOS_Vec2Dec lat)))
      );and
    (list file (list lon lat))
  );if
);defun
 
;; Retrieves and Geo-Locates all geo-tagged photos from user-specified folder.
(defun c:PHOTOS ( / blkName blkDWG defaultBrowseLocation errMsg blkPath strPrompt folder photoLinks lyrName lyrColor e str)
  ;; Variable Inputs
  (setq blkName "IMAGEDIR"        ;<-- the block to insert for all photo locations (do NOT use dynamic block)
        ;;blkDWG "C:\\users\\my folder\\IMAGEDIR.dwg" ;<-- if block not in current dwg, where to find it
blkDWG "C:\\FIMGDR\\IMAGEDIRECTION.dwg"
        lyrName "_Geo-Photos"        ;<-- layer where blocks will be inserted
        lyrColor 11                  ;<-- if necessary to create layer
        defaultBrowseLocation "C:\\" ;<-- the default path that LM's folder browser opens to
  );setq
  ;; Initial check(s)
  (cond
    ((eq "" (getvar 'CGEOCS)) (setq errMsg "\nDrawing must be Geo-Located."))
    ((zerop (getvar 'TILEMODE)) (setq errMsg "\nMust be in Model space layout."))
    ((and (null (tblsearch "BLOCK" blkName))
          (not (and (setq blkPath (findfile blkDWG))
                    (null (progn (command "-INSERT" blkPath) (command))))))
      (setq errMsg (strcat "\nUnable to locate drawing, " blkDWG))
    )
  );cond
  (if errMsg
    (progn (prompt errMsg) (alert errMsg) (exit))
  );if
  ;; Begin work
  (setq strPrompt "Navigate to foldere where you would like to retrieve all Geotagged photos.")
  (if (and (setq folder (LM:browseforfolder strPrompt defaultBrowseLocation 512))
           (setq photoLinks (PHOTOS_GetPhotoLinks folder))
           (setq photoLinks (mapcar 'PHOTOS_GetGeoData photoLinks))
           (setq photoLinks
             (mapcar
               'list
               (mapcar 'car photoLinks)
               (mapcar 'LL->PT (mapcar 'cadr photoLinks))
             );mapcar
           );setq
           (setq photoLinks (vl-remove-if '(lambda (x) (null (cadr x))) photoLinks))
      );and
    (progn
      ;; Be sure layer exists
      (if (not (tblsearch "LAYER" lyrName))
        (entmake (list (cons 0 "LAYER") (cons 100 "AcDbSymbolTableRecord") (cons 100 "AcDbLayerTableRecord")
                 (cons 2 lyrName) (cons 70 0) (cons 62 lyrColor) (cons 290 0)))
      );if
      ;; Place Blocks, Add hyperlink
      (foreach photo photoLinks
        (setq e (entmakex (list (cons 0 "INSERT") (cons 2 blkName) (cons 8 lyrName) (cons 10 (cadr photo)))))
        (vla-add
          (vlax-get-property
            (vlax-ename->vla-object e)
            'Hyperlinks
          );vlax-get-property
          (car photo)
        );vla-add
      );foreach
      (setq str (strcat "\nPHOTOS Complete, " (itoa (length photoLinks)) " photos linked."))
      (prompt str)
      (alert str)
    );progn
  );if
  (princ)
);defun
Message 17 of 24

engsurveymohamed
Observer
Observer

Hi,

 So, I was able to create a block drawing and put it on the right path. Then, I ran the Lisp, and it asked me about the image. I imported image 3953, which is a .JPG file converted from.HEIC (iPhone Image), then the Lisp came back with this error. 

Screenshot 2025-06-20 082856.png

Message 18 of 24

CodeDing
Advisor
Advisor

@engsurveymohamed ,

 

I was able to successfully add your image to a geo-located dwg on the first-try.

 

image.png

image.png

 

You need to adjust these "Variable Inputs" within the code (just update the 'blkDWG' variable to the location of the drawing that contains your camera block):

  ;; Variable Inputs
  (setq blkName "HBD95S_FRONT"        ;<-- the block to insert for all photo locations (do NOT use dynamic block)
        blkDWG "C:\\users\\<username>\\downloads\\myblockdrawing.dwg" ;<-- if block not in current dwg, where to find it
        lyrName "_Geo-Photos"        ;<-- layer where blocks will be inserted
        lyrColor 11                  ;<-- if necessary to create layer
        defaultBrowseLocation "C:\\" ;<-- the default path that LM's folder browser opens to
  );setq

 

Best,

~DD

Message 19 of 24

CodeDing
Advisor
Advisor

@JanardFugro ,

 

The (ExifData ...) function was not correctly retrieving all of the image properties because one of the properties appeared to be faulty or corrupt. I have updated the (ExifData ...) function and re-attached the code for you to use. Be sure to still update the Variable Inputs section again.

 

Best,

~DD

Message 20 of 24

engsurveymohamed
Observer
Observer

Thank you so much Now its working perfectly