├── PythonPhoto2Location.py ├── README.md ├── python-logo.png ├── requirements.txt └── window_icon.ico /PythonPhoto2Location.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import datetime 3 | import glob 4 | import os 5 | import os.path 6 | import tkinter 7 | import webbrowser 8 | from decimal import Decimal 9 | from threading import Thread 10 | from tkinter import * 11 | from tkinter import filedialog 12 | from typing import Optional, Any 13 | import country_converter as coco 14 | import pandas as pd 15 | import reverse_geocoder as rg 16 | from PIL import Image 17 | from PIL.ExifTags import GPSTAGS 18 | from PIL.ExifTags import TAGS 19 | from gmplot import gmplot 20 | 21 | # ADD YOUR OWN GOOGLE MAPS API KEY (the one below is a fake ID) 22 | google_api_key = "IzaSyD2KMkrfQkzqNBEo-5iZDhDOlbvvDrO0dM" 23 | 24 | # Initialize the main window and all components 25 | window = tkinter.Tk() 26 | window.minsize(500, 500) 27 | window.title("Photo To Location") 28 | #window.wm_iconbitmap("window_icon.ico") 29 | entryText = tkinter.StringVar() 30 | textbox = tkinter.Entry(window, width=75, textvariable=entryText) 31 | textbox.place(x=10, y=20) 32 | link2 = Label(window, text="", fg="blue", cursor="hand2") 33 | link2.place(x=160, y=90) 34 | link1 = Label(window, text="", fg="blue", cursor="hand2") 35 | link1.place(x=270, y=90) 36 | label = tkinter.Label(window, text="") 37 | label.place(x=10, y=80) 38 | label2 = tkinter.Label(window, text="") 39 | label2.place(x=10, y=96) 40 | status_message = StringVar() 41 | status = Label(window, textvariable=status_message, bd=1, relief=SUNKEN, anchor=W) 42 | status.pack(side=BOTTOM, fill=X) 43 | text = Text(window, height=22, width=59) 44 | text.place(x=10, y=120) 45 | cpt = 0 46 | 47 | 48 | def get_labeled_exif(exif): 49 | labeled = {} 50 | for (key, val) in exif.items(): 51 | labeled[TAGS.get(key)] = val 52 | return labeled 53 | 54 | 55 | def get_exif(filename): 56 | image: Optional[Any] = Image.open(filename) 57 | image.verify() 58 | return image._getexif() 59 | 60 | 61 | def get_geotagging(exif): 62 | if not exif: 63 | raise ValueError("No EXIF metadata found") 64 | geo_tagging = {} 65 | for (idx, tag) in TAGS.items(): 66 | if tag == 'GPSInfo': 67 | if idx not in exif: 68 | # raise ValueError("No EXIF geo_tagging found") 69 | error = 1 70 | for (key, val) in GPSTAGS.items(): 71 | if key in exif[idx]: 72 | geo_tagging[val] = exif[idx][key] 73 | return geo_tagging 74 | 75 | 76 | def get_decimal_from_dms(dms, ref): 77 | degrees = dms[0][0] / dms[0][1] 78 | minutes = dms[1][0] / dms[1][1] / 60.0 79 | seconds = dms[2][0] / dms[2][1] / 3600.0 80 | if ref in ['S', 'W']: 81 | degrees = -degrees 82 | minutes = -minutes 83 | seconds = -seconds 84 | return round(degrees + minutes + seconds, 5) 85 | 86 | 87 | def get_coordinates(geotags): 88 | lat = get_decimal_from_dms(geotags['GPSLatitude'], geotags['GPSLatitudeRef']) 89 | lon = get_decimal_from_dms(geotags['GPSLongitude'], geotags['GPSLongitudeRef']) 90 | return lat, lon 91 | 92 | 93 | def converter(date_time): 94 | format = '%Y:%m:%d' # The format 95 | datetime_str = datetime.datetime.strptime(date_time, format) 96 | return datetime_str 97 | 98 | 99 | # Function to find the screen dimensions, calculate the center and set geometry 100 | def center(win): 101 | win.update_idletasks() 102 | width = win.winfo_width() 103 | height = win.winfo_height() 104 | x = (win.winfo_screenwidth() // 2) - (width // 2) 105 | y = (win.winfo_screenheight() // 2) - (height // 2) 106 | win.geometry('{}x{}+{}+{}'.format(width, height, x, y)) 107 | 108 | 109 | def ask_quit(): 110 | window.destroy() 111 | exit() 112 | 113 | 114 | def on_closing(): 115 | root = Tk() 116 | root.destroy() 117 | window.destroy() 118 | exit() 119 | 120 | 121 | def open_file_dialog(): 122 | global cpt 123 | root = Tk() 124 | root.withdraw() 125 | folder_selected = filedialog.askdirectory() 126 | entryText.set(folder_selected) 127 | print("Directory to Process: " + folder_selected) 128 | cpt = sum([len(files) for r, d, files in os.walk(folder_selected)]) 129 | print("Number of Files using listdir method#1 :", cpt) 130 | 131 | 132 | def open_excel(event): 133 | os.startfile("results.xlsx") 134 | 135 | 136 | def percentage(part, whole): 137 | return round(100 * float(part) / float(whole), 2) 138 | 139 | 140 | def start_thread(): 141 | Thread(target=process, 142 | daemon=True).start() 143 | 144 | 145 | def callback(url): 146 | webbrowser.open_new(url) 147 | 148 | 149 | # Define button press function 150 | def process(): 151 | print("Starting to Parse Image Exif Information") 152 | global cpt 153 | global status_message 154 | status_message.set("") 155 | link1.config(text="") 156 | link2.config(text="") 157 | text.delete('1.0', END) 158 | status.config(text="") 159 | count = 0 160 | path = entryText.get() + "/" 161 | # path = entryText.get().replace("/", "//")+"//" 162 | files = [f for f in glob.glob(path + "**/*.jpg", recursive=True)] 163 | visited_cities = [] 164 | visited_cities_clean = [] 165 | visited_coordinates_lat = [] 166 | visited_coordinates_lon = [] 167 | visited_coordinates = [] 168 | months = [] 169 | years = [] 170 | cities = [] 171 | countries = [] 172 | path_directory = "" 173 | 174 | for f in files: 175 | f = f.replace("\\", "/") 176 | count = count + 1 177 | if count % 10 == 0: 178 | status_message.set( 179 | "Processing Image: " + str(count) + " of " + str(cpt) + " (" + str(percentage(count, cpt)) + "%)") 180 | try: 181 | exif = get_exif(f) 182 | geo_tags = get_geotagging(exif) 183 | coordinates = get_coordinates(geo_tags) 184 | lat = float(Decimal(coordinates[0]).quantize(Decimal(10) ** -3)) 185 | lon = float(Decimal(coordinates[1]).quantize(Decimal(10) ** -3)) 186 | results = rg.search(coordinates, mode=1) 187 | city = results[0].get('name') + ", " 188 | state = results[0].get('admin1') + ", " 189 | country = results[0].get('cc') 190 | cc = coco.CountryConverter(include_obsolete=True) 191 | country = cc.convert(country, to='name_short') 192 | 193 | try: 194 | datum = geo_tags['GPSDateStamp'] 195 | year = str(converter(datum).year) 196 | month = str(converter(datum).month) 197 | except: 198 | try: 199 | datum = str(Image.open(f)._getexif()[36867]).split(" ", 1)[0] 200 | year = str(converter(datum).year) 201 | month = str(converter(datum).month) 202 | except: 203 | year = "1970" 204 | month = "00" 205 | 206 | if len(year) == 1: 207 | year = "0" + year 208 | if len(month) == 1: 209 | month = "0" + month 210 | 211 | # print(f) 212 | if year != "1970" and month != "00": 213 | visited_cities.append(year + ":" + month + " | " + city + state + country) 214 | 215 | # print("Location:" + city + state + country + " | " + f) 216 | for word in visited_cities: 217 | if word not in visited_cities_clean: 218 | visited_cities_clean.append(word) 219 | label.config(text=f"Processing Coordinates: {coordinates}") 220 | label2.config(text="Successfully Resolved: " + city + state + country) 221 | if lat != 0.000 and lon != 0.000: 222 | visited_coordinates_lat.append(lat) 223 | visited_coordinates_lon.append(lon) 224 | pathr, filenamer = f.rsplit('/', 1) 225 | visited_coordinates.append( 226 | city + country + "|" + str(lat) + "|" + str(lon) + "|" + year + ":" + month + "|" + pathr) 227 | months.append(month) 228 | years.append(year) 229 | cities.append(results[0].get('name')) 230 | countries.append(country) 231 | text.insert(tkinter.END, 232 | year + "/" + month + " - " + city + country + "\n") 233 | text.see("end") 234 | 235 | 236 | except: 237 | # print("GPS Data Missing in " + f) 238 | error = 2 239 | 240 | status_message.set("Processed: " + str(count) + " images") 241 | label.config(text="") 242 | label2.config(text="") 243 | print("--- GOOGLE MAP Generated ---") 244 | google_map = gmplot.GoogleMapPlotter(0, 0, 2) 245 | google_map.coloricon = "http://www.googlemapsmarkers.com/v1/%s/" 246 | google_map.apikey = google_api_key 247 | google_map.heatmap(visited_coordinates_lat, visited_coordinates_lon) 248 | google_map.plot(visited_coordinates_lat, visited_coordinates_lon, c='#046CC6', edge_width=1.0) 249 | 250 | # ADD MARKERS 251 | for word in visited_coordinates: 252 | title = word.split("|")[0] 253 | lati = word.split("|")[1] 254 | long = word.split("|")[2] 255 | date = word.split("|")[3] 256 | path_directory = word.split("|")[4] 257 | date = date.split(":") 258 | month_word = (calendar.month_name[int(date[1])]) 259 | google_map.marker(float(lati), float(long), 'cornflowerblue', 260 | title=str(title) + " (" + str(date[0]) + " " + str(month_word) + ")") 261 | 262 | google_map.draw("result.html") 263 | 264 | link1.config(text="Open Map") 265 | link1.bind("", lambda e: callback("result.html")) 266 | 267 | link2.config(text="Open Excel") 268 | link2.bind("", open_excel) 269 | 270 | # EXCEL 271 | df = pd.DataFrame( 272 | {'Month': months, 'Year': years, 'City': cities, 'Country': countries, 'Lat.': visited_coordinates_lat, 273 | 'Long.': visited_coordinates_lon, 'Directory': path_directory}) 274 | writer = pd.ExcelWriter("results.xlsx", engine='xlsxwriter') 275 | df.to_excel(writer, sheet_name='Sheet1', startrow=1, header=False) 276 | workbook = writer.book 277 | worksheet = writer.sheets['Sheet1'] 278 | 279 | header_format = workbook.add_format( 280 | {'bold': True, 'text_wrap': False, 'valign': 'top', 'fg_color': '#D7E4BC', 'border': 1}) 281 | 282 | # Write the column headers with the defined format. 283 | for col_num, value in enumerate(df.columns.values): 284 | worksheet.write(0, col_num + 1, value, header_format) 285 | 286 | # Close the Pandas Excel writer and output the Excel file. 287 | df.sort_values(['Month', 'Year'], ascending=[True, True]) 288 | writer.save() 289 | 290 | # Write END to Textbox 291 | text.insert(tkinter.END, "\n") 292 | text.insert(tkinter.END, "---------------END---------------\n") 293 | text.insert(tkinter.END, "\n") 294 | text.see("end") 295 | print("-------------END------------") 296 | 297 | 298 | # Place 'Change Label' button on the window 299 | button = tkinter.Button(window, text="...", command=open_file_dialog) 300 | button.place(x=470, y=17) 301 | process_button = tkinter.Button(window, text="Process Images", command=start_thread) 302 | process_button.place(x=10, y=50) 303 | 304 | # Center Window on Screen 305 | center(window) 306 | 307 | # close the program and tkinter window on exit 308 | window.protocol("WM_DELETE_WINDOW", ask_quit) 309 | # Show new window 310 | window.mainloop() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Photo2Location - About 2 | Convert all your PHOTOS to travel log of visited Locations in Google Maps and MS Excel format. 3 | 4 | # Blog post 5 | Blog post about development of this app and visualizing JPEG Exif using Python. 6 | How to convert JPEG Meta-Data into Excel Travel Log and Google Map of All Visited Places: 7 | https://www.joe0.com/2019/09/28/visualizing-jpeg-exif-using-python-how-to-convert-jpeg-meta-data-into-excel-travel-log-and-google-map-of-all-visited-places/ 8 | 9 | # Reddit 10 | If you like it, up-vote this app on Reddit: https://www.reddit.com/r/Python/comments/dan1eo/just_made_a_python_app_to_convert_thousands_of_my 11 | 12 | # TODO 13 | - Image Thumbnails in Google Maps preview 14 | - Add high level directory paths to Excel travel log 15 | 16 | # Notes 17 | - Important: To create Google Map, you must ADD YOUR OWN GOOGLE MAPS API KEY (the one in the code is a fake ID) 18 | - Import the dependencies list listed in requirements.txt -------------------------------------------------------------------------------- /python-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JozefJarosciak/PythonPhoto2Location/e3b4dccb78290d35c2dbe1232529c622fed5eeae/python-logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.9.11 2 | chardet==3.0.4 3 | country-converter==0.6.6 4 | docopt==0.6.2 5 | gmplot==1.2.0 6 | idna==2.8 7 | numpy==1.17.2 8 | pandas==0.25.1 9 | Pillow==6.1.0 10 | pipreqs==0.4.9 11 | python-dateutil==2.8.0 12 | pytz==2019.2 13 | requests==2.22.0 14 | reverse-geocoder==1.5.1 15 | scipy==1.3.1 16 | six==1.12.0 17 | typing==3.7.4.1 18 | urllib3==1.25.6 19 | XlsxWriter==1.2.1 20 | yarg==0.1.9 -------------------------------------------------------------------------------- /window_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JozefJarosciak/PythonPhoto2Location/e3b4dccb78290d35c2dbe1232529c622fed5eeae/window_icon.ico --------------------------------------------------------------------------------