Skip to content

Instantly share code, notes, and snippets.

@akleemans
Created October 15, 2024 18:32
Show Gist options
  • Save akleemans/35f3b1291717e476d4f22a6fe614f4a2 to your computer and use it in GitHub Desktop.
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
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