|
from fastapi import FastAPI |
|
from pydantic import BaseModel, Field |
|
from typing import Literal |
|
import json |
|
import numpy as np |
|
import onnxruntime as ort |
|
from typing_extensions import Annotated |
|
import gradio as gr |
|
from cryptography.fernet import Fernet |
|
import os |
|
|
|
|
|
key = os.getenv("ONNX_KEY") |
|
cipher = Fernet(key) |
|
|
|
VERSION = "0.0.1" |
|
TITLE = f"DVPI beregnings API (version {VERSION})" |
|
DESCRIPTION = "Beregn Dansk Vandløbs Plante Indeks (DVPI) fra dækningsgrad af plantearter. Beregningen er baseret på en model som efterligner DVPI beregningsmetoden og er dermed ikke eksakt, usikkerheden er i gennemsnit **±0.05 EQR-enheder**." |
|
URL = "https://kennethtm-dvpi.hf.space" |
|
|
|
|
|
with open("model.bin", "rb") as f: |
|
encrypted = f.read() |
|
decrypted = cipher.decrypt(encrypted) |
|
ort_session = ort.InferenceSession(decrypted) |
|
|
|
with open("spec2idx.json", "r") as f: |
|
spec2idx = json.load(f) |
|
|
|
|
|
valid_species = tuple(spec2idx.keys()) |
|
|
|
class SpeciesCover(BaseModel): |
|
species: dict[Literal[valid_species], Annotated[float, Field(ge=0, le=100)]] |
|
|
|
model_config = { |
|
"json_schema_extra": { |
|
"examples": [{ |
|
"species": { |
|
"Potamogeton alpinus": 25.0, |
|
"Berula erecta": 15.5, |
|
"Calamagrostis canescens": 10.0 |
|
} |
|
}] |
|
} |
|
} |
|
|
|
|
|
class EQRResult(BaseModel): |
|
EQR: float |
|
DVPI: int |
|
version: str = VERSION |
|
|
|
|
|
app = FastAPI(title=TITLE, |
|
description=DESCRIPTION) |
|
|
|
def eqr_to_dvpi(eqr: float) -> int: |
|
if eqr < 0.20: |
|
return 1 |
|
elif eqr < 0.35: |
|
return 2 |
|
elif eqr < 0.50: |
|
return 3 |
|
elif eqr < 0.70: |
|
return 4 |
|
else: |
|
return 5 |
|
|
|
|
|
@app.post("/dvpi") |
|
def predict(cover_data: SpeciesCover) -> EQRResult: |
|
"""Predict EQR and DVPI from species cover data""" |
|
|
|
input_vector = np.zeros((1, len(spec2idx))) |
|
|
|
print(cover_data.species) |
|
|
|
|
|
for species, cover in cover_data.species.items(): |
|
idx = spec2idx[species] |
|
input_vector[0, idx] = cover |
|
|
|
|
|
input_name = ort_session.get_inputs()[0].name |
|
ort_inputs = {input_name: input_vector.astype(np.float32)} |
|
ort_output = ort_session.run(None, ort_inputs) |
|
|
|
eqr = float(ort_output[0][0]) |
|
dvpi = eqr_to_dvpi(eqr) |
|
|
|
return EQRResult(EQR=round(eqr, 2), DVPI=dvpi) |
|
|
|
@app.get("/arter") |
|
def list_species() -> dict: |
|
"""Return list of valid species names""" |
|
return {"species": list(spec2idx.keys())} |
|
|
|
|
|
def add_entry(species, cover, current_dict) -> tuple[SpeciesCover, str]: |
|
|
|
current_dict[species] = cover |
|
|
|
return current_dict, current_dict |
|
|
|
def gradio_predict(cover_data: dict): |
|
|
|
if len(cover_data) == 0: |
|
return {} |
|
|
|
data = SpeciesCover(species=cover_data) |
|
result = predict(data) |
|
|
|
return result.model_dump() |
|
|
|
with gr.Blocks() as io: |
|
|
|
gr.Markdown(f"# {TITLE}") |
|
gr.Markdown(DESCRIPTION) |
|
|
|
with gr.Tab(label = "Beregner"): |
|
|
|
gr.Markdown("Beregning er baseret på samfund af plantearter og deres dækningsgrad. Dækningsgraden angives i procent som summen af scoren for dækningsgraden (1-5) divideret med det samlede antal undersøgte kvadrater gange 5, og til sidste konverteret til procent. Eksempel: Potamogeton alpinus findes 3 felter med scorerne 2, 3 og 5 ud af 50 undersøgte kvadrater. Dækningsgraden for Potamogeton alpinus er derfor (2+3+5)/(50*5)*100 = 4%.") |
|
|
|
current_dict = gr.State({}) |
|
|
|
with gr.Row(): |
|
species_input = gr.Dropdown(choices=valid_species, label="Vælg art") |
|
cover_input = gr.Number(label="Dækningsgrad (%)", minimum=0, maximum=100) |
|
|
|
with gr.Row(): |
|
add_btn = gr.Button("Tilføj") |
|
reset_btn = gr.Button("Nulstil") |
|
|
|
list_display = gr.JSON(label="Artsliste") |
|
|
|
calc_btn = gr.Button("Beregn") |
|
results = gr.JSON(label="Resultater") |
|
|
|
def reset_dict(): |
|
return {}, {}, {} |
|
|
|
add_btn.click( |
|
add_entry, |
|
inputs=[species_input, cover_input, current_dict], |
|
outputs=[current_dict, list_display] |
|
) |
|
|
|
reset_btn.click( |
|
reset_dict, |
|
inputs=[], |
|
outputs=[current_dict, list_display, results] |
|
) |
|
|
|
calc_btn.click( |
|
gradio_predict, |
|
inputs=[current_dict], |
|
outputs=results |
|
) |
|
|
|
gr.Markdown("App og model af Kenneth Thorø Martinsen.") |
|
|
|
with gr.Tab(label="Dokumentation"): |
|
|
|
|
|
gr.Markdown("## Eksempel på brug af API") |
|
gr.Markdown(f"API dokumentation kan findes på [{URL}/docs]({URL}/docs)") |
|
gr.Markdown("### Python") |
|
gr.Code(f""" |
|
import requests |
|
import json |
|
|
|
data = {{ |
|
"species": {{ |
|
"Potamogeton alpinus": 25.0, |
|
"Berula erecta": 15.5, |
|
"Calamagrostis canescens": 10.0 |
|
}} |
|
}} |
|
|
|
response = requests.post("{URL}/dvpi", json=data) |
|
print(response.json()) |
|
""") |
|
|
|
gr.Markdown("### R") |
|
gr.Code(f""" |
|
library(httr) |
|
library(jsonlite) |
|
|
|
data <- list(species = list( |
|
"Potamogeton alpinus" = 25.0, |
|
"Berula erecta" = 15.5, |
|
"Calamagrostis canescens" = 10.0 |
|
)) |
|
|
|
response <- POST("{URL}/dvpi", |
|
body = toJSON(data, auto_unbox = TRUE), |
|
content_type("application/json")) |
|
|
|
print(fromJSON(rawToChar(response$content))) |
|
""") |
|
|
|
|
|
app = gr.mount_gradio_app(app, io, path="/") |
|
|