Created
October 15, 2024 18:32
-
-
Save akleemans/35f3b1291717e476d4f22a6fe614f4a2 to your computer and use it in GitHub Desktop.
Create a plant diary from an excursion using the Pl@ntNet API. More details at https://kleemans.ch/classifying-plants-creating-an-excursion-diary
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import List, Tuple | |
import requests | |
import json | |
import glob | |
import os | |
from PIL import Image, ImageOps | |
""" | |
This script will automically pick up images from a subfolder 'img' and try to classify them against | |
the Pl@ntNet API. (You have to provide your API key below.) | |
It will then create a HTML file with a list of plants (with images downscaled) and their classification, | |
including a small map if geo-coordinates were found. | |
Last updated: 2024-10-14 | |
https://kleemans.ch/classifying-plants-creating-an-excursion-diary | |
""" | |
api_key = '<api-key>' | |
template = """ | |
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css"> | |
<title>{{title}}</title> | |
<style> | |
body { | |
width: 1000px; | |
} | |
.plant-img { | |
width: 400px; | |
height: 400px; | |
object-fit: cover; | |
} | |
#map { | |
height: 350px; | |
width: 700px; | |
margin-left: auto; | |
margin-right: auto; | |
margin-bottom: 50px; | |
} | |
</style> | |
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css" crossorigin="" | |
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="/> | |
</head> | |
<body> | |
<h1>{{title}}</h1> | |
<p> | |
(Beschreibung Ausflug) | |
</p> | |
<div id="map"></div> | |
<h2>Pflanzen</h2> | |
<table> | |
{{table_content}} | |
</table> | |
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js" crossorigin="" | |
integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="></script> | |
<script> | |
const map = L.map('map').setView({{gps_data}}, 10); | |
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
maxZoom: 19, attribution: '© OpenStreetMap'}).addTo(map); | |
L.marker({{gps_data}}).addTo(map); | |
</script> | |
</body> | |
</html> | |
""" | |
tr_template = """ | |
<tr> | |
<td><img class="plant-img" src="{{filename}}"></td> | |
<td> | |
{{names}}</td> | |
</tr> | |
""" | |
# Latin names of plants of interest can be added here, they will be shown on top | |
z200 = [ | |
"Abies alba", | |
"Acer campestre", | |
# etc. | |
] | |
api_service = 'https://my-api.plantnet.org/v2/identify' | |
api_project = 'weurope' | |
api_lang = 'de' | |
base_url = f'{api_service}/{api_project}?api-key={api_key}&lang={api_lang}' | |
basewidth = 1280 | |
leaf_recognizer = '_l' | |
img_dir = 'img/' | |
scaled_img_dir = 'img_scaled/' | |
def downscale(): | |
os.mkdir('img_scaled') | |
for filename in glob.glob(f'{img_dir}*'): | |
print('Downscaling', filename) | |
img = Image.open(filename) | |
img = ImageOps.exif_transpose(img) | |
w = img.size[0] | |
h = img.size[1] | |
factor = max(w, h) / basewidth | |
img = img.resize((int(w/factor), int(h/factor)), Image.LANCZOS) | |
new_filename = filename.replace(img_dir, scaled_img_dir) | |
img.save(new_filename) | |
def decimal_coords(ref: str, coords: Tuple[float, float, float]): | |
decimal_degrees = coords[0] + coords[1] / 60.0 + coords[2] / 3600.0 | |
if ref == 'S' or ref == 'W': | |
decimal_degrees = -decimal_degrees | |
return decimal_degrees | |
def get_coordinates() -> Tuple[float, float]: | |
for filename in glob.glob(f'{img_dir}*'): | |
exif = Image.open(filename)._getexif() | |
if exif is not None: | |
entry = list(filter(lambda x: x[0] == 34853, exif.items())) | |
gps_info = entry[0][1] | |
try: | |
lat = decimal_coords(gps_info[1], gps_info[2]) | |
long = decimal_coords(gps_info[3], gps_info[4]) | |
return [round(lat, 8), round(long, 8)] | |
except: | |
continue | |
return (0.0, 0.0) | |
def get_species(filename: str) -> List[str]: | |
organ = 'leaf' if leaf_recognizer in filename else 'flower' | |
files = {'images': open(filename, 'rb')} | |
response = requests.post(base_url, files=files, data={'organs': [organ]}) | |
json_result = json.loads(response.text) | |
names = [] | |
for result in json_result['results']: | |
# print('result:', result) | |
score = result['score'] | |
if score < 0.05: | |
break | |
lat_name = result['species']['scientificNameWithoutAuthor'] | |
common_name = '-' | |
if len(result['species']['commonNames']) > 0: | |
common_name = result['species']['commonNames'][0] | |
names.append([lat_name + ' (' + str(score * 100)[:4] + '%)', common_name]) | |
return names | |
def get_table_content() -> str: | |
table_content = '' | |
for filename in glob.glob(f'{scaled_img_dir}*'): | |
print('Identifying', filename) | |
names_str = '' | |
is_z200 = False | |
for name in get_species(filename): | |
lat_name = name[0].split(' (')[0] | |
names_str += f'<div><b>{name[0]}</b></div>\n' | |
names_str += f'<div>{name[1]}</div>\n' | |
if ' '.join(lat_name.split(' ')[:2]) in z200: | |
is_z200 = True | |
names_str += '<div><i>CH-200</i></div>\n' | |
tr_entry = tr_template.replace('{{filename}}', filename).replace('{{names}}', names_str) | |
if is_z200: | |
table_content = tr_entry + table_content | |
else: | |
table_content += tr_entry | |
return table_content | |
### | |
# Get GPS data | |
gps_data = get_coordinates() | |
# Downscale all images | |
downscale() | |
table_content = get_table_content() | |
print('Writing file...') | |
title = os.getcwd().split('/')[-1] | |
html = template.replace('{{table_content}}', table_content) \ | |
.replace('{{gps_data}}', str(gps_data)).replace('{{title}}', title) | |
with open('index.html', 'w') as write_file: | |
write_file.write(html) | |
print('Done!') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment