KennethTM commited on
Commit
ec92a3d
·
verified ·
1 Parent(s): 7ebc378

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +13 -0
  2. idx_to_target.json +1 -0
  3. index.html +232 -0
  4. main.py +228 -0
  5. model.bin +3 -0
  6. requirements.txt +9 -0
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
idx_to_target.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"hovednaturtype": {"0": "Ferskeng", "1": "Hede", "2": "Mose og K\u00e6r", "3": "Overdrev", "4": "Skov", "5": "Strandenge, strandsumpe", "6": "Strandklit", "7": "S\u00f8"}, "arealet_nbl": {"0": "ja", "1": "nej"}, "naturtilstand": {"0": "I", "1": "II", "2": "III", "3": "IV", "4": "V"}}
index.html ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Paragraf 3 screener</title>
5
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
6
+ <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
9
+ <style>
10
+ html, body, #map {
11
+ height: 100%;
12
+ width: 100%;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ #infoBox {
18
+ position: absolute;
19
+ top: 10px;
20
+ right: 10px;
21
+ background-color: rgba(255, 255, 255, 0.8);
22
+ padding: 10px;
23
+ border-radius: 5px;
24
+ z-index: 1000;
25
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
26
+ font-family: Arial, sans-serif;
27
+ text-align: center;
28
+ max-width: 300px;
29
+ }
30
+
31
+ /* Loading spinner styles */
32
+ .spinner {
33
+ position: absolute;
34
+ width: 40px;
35
+ height: 40px;
36
+ margin: 0;
37
+ background-color: rgba(255, 255, 255, 0.8);
38
+ border-radius: 50%;
39
+ border: 3px solid transparent;
40
+ border-top-color: #3498db;
41
+ border-bottom-color: #3498db;
42
+ animation: spin 2s linear infinite;
43
+ z-index: 1000;
44
+ }
45
+
46
+ /* Add this if not already present */
47
+ @keyframes spin {
48
+ 0% { transform: rotate(0deg); }
49
+ 100% { transform: rotate(360deg); }
50
+ }
51
+
52
+ .spinner-container {
53
+ background: none !important;
54
+ }
55
+
56
+ /* Make sure there's no Leaflet default icon background */
57
+ .leaflet-div-icon {
58
+ background: transparent;
59
+ border: none;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div id="map"></div>
65
+ <div id="infoBox">
66
+ <h3 style="margin: 0 0 5px 0;">Paragraf 3 screener</h3>
67
+ <p style="margin: 0;">Tegn et område på kortet og bestem <b>hovednaturtype</b>, <b>naturtilstand</b> og <b>om det er omfattet af §3</b></p>
68
+ <br>
69
+ <p style="margin: 0;">Udviklet af:<br>Kenneth Thorø Martinsen (kenneth2810@gmail.com)</p>
70
+ </div>
71
+ <script>
72
+ var map = L.map('map').setView([56.2, 11.5], 8);
73
+
74
+ L.tileLayer('https://services.datafordeler.dk/GeoDanmarkOrto/orto_foraar_webm/1.0.0/WMTS/orto_foraar_webm/default/DFD_GoogleMapsCompatible/{z}/{y}/{x}.jpg?username=BJSIGPGRVW&password=Panseryrtat*56klinge', {
75
+ attribution: 'CC BY 4.0, GeoDanmark, Forårsbilleder Ortofoto, dataforsyningen.dk',
76
+ maxZoom: 19
77
+ }).addTo(map);
78
+
79
+ var drawnItems = new L.FeatureGroup();
80
+ map.addLayer(drawnItems);
81
+
82
+ var drawControl = new L.Control.Draw({
83
+ draw: {
84
+ polygon: true,
85
+ polyline: false,
86
+ circle: false,
87
+ rectangle: false,
88
+ marker: false,
89
+ circlemarker: false
90
+ },
91
+ edit: {
92
+ featureGroup: drawnItems
93
+ }
94
+ });
95
+ map.addControl(drawControl);
96
+
97
+ map.on('draw:created', function (e) {
98
+ var layer = e.layer;
99
+ drawnItems.addLayer(layer);
100
+ predictAndShow(layer);
101
+ // Click handler will be set in predictAndShow after the data is loaded
102
+ });
103
+
104
+ map.on('draw:edited', function(e){
105
+ var layers = e.layers;
106
+ layers.eachLayer(function(layer) {
107
+ predictAndShow(layer);
108
+ });
109
+ });
110
+
111
+
112
+ function predictAndShow(layer) {
113
+ var geojson = layer.toGeoJSON();
114
+
115
+ // Get the center of the polygon for placing the spinner
116
+ var bounds = layer.getBounds();
117
+ var center = bounds.getCenter();
118
+
119
+ // Create a more visible spinner with custom HTML
120
+ var spinnerHtml = '<div class="spinner" style="width: 25px; height: 25px; ' +
121
+ 'border: 5px solid #f3f3f3; border-top: 5px solid #3498db; ' +
122
+ 'border-radius: 50%; animation: spin 2s linear infinite;"></div>';
123
+
124
+ var spinner = L.divIcon({
125
+ html: spinnerHtml,
126
+ className: 'spinner-container',
127
+ iconSize: [50, 50],
128
+ iconAnchor: [25, 25] // Center the spinner on the point
129
+ });
130
+
131
+ // Add the spinner to the map
132
+ var loadingMarker = L.marker(center, {
133
+ icon: spinner,
134
+ interactive: false,
135
+ zIndexOffset: 1000 // Ensure spinner appears above other elements
136
+ }).addTo(map);
137
+
138
+ // Change the polygon style to indicate loading
139
+ var originalStyle = {
140
+ color: layer.options.color || '#3388ff',
141
+ fillOpacity: layer.options.fillOpacity || 0.2
142
+ };
143
+
144
+ layer.setStyle({
145
+ fillOpacity: 0.1,
146
+ color: '#aaa'
147
+ });
148
+
149
+ fetch('/predict', {
150
+ method: 'POST',
151
+ headers: {
152
+ 'Content-Type': 'application/json'
153
+ },
154
+ body: JSON.stringify({ geojson: geojson })
155
+ })
156
+ .then(response => response.json())
157
+ .then(data => {
158
+ // Remove the spinner
159
+ map.removeLayer(loadingMarker);
160
+ // Restore original style
161
+ layer.setStyle(originalStyle);
162
+
163
+ // Create popup content from data
164
+ var popupContent = "<div><strong>Resultat:</strong><br>";
165
+ popupContent += data.result;
166
+ popupContent += "</div>";
167
+
168
+ // Store the result in layer options for future reference
169
+ layer.options.result = data.result;
170
+
171
+ // Create popup content
172
+ var popupContent = "<div><strong>Resultat:</strong><br>";
173
+ popupContent += data.result;
174
+ popupContent += "</div>";
175
+
176
+ // Create popup configuration
177
+ var popup = L.popup({
178
+ closeButton: true,
179
+ autoClose: false,
180
+ closeOnEscapeKey: false,
181
+ closeOnClick: false
182
+ });
183
+
184
+ // Remove any existing click handlers
185
+ layer.off('click');
186
+
187
+ // Unbind any existing popups
188
+ if (layer.getPopup()) {
189
+ layer.unbindPopup();
190
+ }
191
+
192
+ // Bind the popup with content and open it
193
+ layer.bindPopup(popup)
194
+ .setPopupContent(popupContent)
195
+ .openPopup();
196
+ // Store the result in layer options for future reference
197
+ layer.options.result = data.result;
198
+
199
+ // Create popup content
200
+ var popupContent = "<div><strong>Resultat:</strong><br>";
201
+ popupContent += data.result;
202
+ popupContent += "</div>";
203
+
204
+ // Create popup configuration
205
+ var popup = L.popup({
206
+ closeButton: true,
207
+ autoClose: false,
208
+ closeOnEscapeKey: false,
209
+ closeOnClick: false
210
+ }).setContent(popupContent);
211
+
212
+ // Remove any existing click handlers
213
+ layer.off('click');
214
+
215
+ // Unbind any existing popups
216
+ if (layer.getPopup()) {
217
+ layer.unbindPopup();
218
+ }
219
+
220
+ // Bind the popup with content and open it
221
+ layer.bindPopup(popup);
222
+ layer.openPopup();
223
+
224
+ // Add a click handler to toggle the popup
225
+ layer.on('click', function(e) {
226
+ layer.openPopup(layer.getBounds().getCenter());
227
+ });
228
+ });
229
+ }
230
+ </script>
231
+ </body>
232
+ </html>
main.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ from typing import Dict, Any
5
+ import numpy as np
6
+ from PIL import Image, ImageDraw
7
+ import json
8
+ from dotenv import load_dotenv
9
+ import os
10
+ import requests
11
+ from io import BytesIO
12
+ from pyproj import Transformer
13
+ import onnxruntime as ort
14
+ from cryptography.fernet import Fernet
15
+ from fastapi.responses import HTMLResponse
16
+
17
+ load_dotenv()
18
+
19
+ app = FastAPI()
20
+
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"], # Allows all origins
24
+ allow_credentials=True,
25
+ allow_methods=["*"], # Allows all methods
26
+ allow_headers=["*"], # Allows all headers
27
+ )
28
+
29
+ # Model load
30
+ key = os.getenv("CLASSIF_MODEL")
31
+ cipher = Fernet(key)
32
+
33
+ with open("model.bin", "rb") as f:
34
+ bin_data = f.read()
35
+ data = cipher.decrypt(bin_data)
36
+ model = ort.InferenceSession(data)
37
+
38
+ #model = ort.InferenceSession("model.onnx")
39
+
40
+ with open("idx_to_target.json", "r") as f:
41
+ idx_to_target = json.load(f)
42
+
43
+ transformer = Transformer.from_crs("EPSG:4326", "EPSG:25832", always_xy=True)
44
+
45
+ IMAGE_SIZE = 384
46
+
47
+ def normalize_image(image,
48
+ mean=(0.485, 0.456, 0.406, 0.5, 0.5),
49
+ std=(0.229, 00.224, 0.225, 0.5, 0.5)):
50
+
51
+ image = (image / 255.0).astype("float32")
52
+
53
+ for i in range(image.shape[2]):
54
+ image[:, :, i] = (image[:, :, i] - mean[i]) / std[i]
55
+
56
+ return image
57
+
58
+ def pad_if_needed(image, target_size):
59
+ height, width, _ = image.shape
60
+
61
+ y0 = abs((height - target_size) // 2)
62
+ x0 = abs((width - target_size) // 2)
63
+
64
+ background = np.zeros((target_size, target_size, 5), dtype="uint8")
65
+ background[y0:(y0 + height), x0:(x0 + width), :] = image
66
+
67
+ return background
68
+
69
+ def softmax(x):
70
+ return np.exp(x) / np.sum(np.exp(x), axis=1)
71
+
72
+ def get_image(coords, max_dim: int) -> Image:
73
+
74
+ coords_utm = [transformer.transform(lon, lat) for lon, lat in coords]
75
+
76
+ xs, ys = zip(*coords_utm)
77
+
78
+ xmin, ymin, xmax, ymax = min(xs), min(ys), max(xs), max(ys)
79
+
80
+ roi_width = xmax - xmin
81
+ roi_height = ymax - ymin
82
+ aspect_ratio = roi_width / roi_height
83
+
84
+ if aspect_ratio > 1:
85
+ width = max_dim
86
+ height = int(max_dim / aspect_ratio)
87
+ else:
88
+ width = int(max_dim * aspect_ratio)
89
+ height = max_dim
90
+
91
+ # Construct WMS parameters
92
+ wms_params = {
93
+ 'username': os.getenv('WMSUSER'),
94
+ 'password': os.getenv('WMSPW'),
95
+ 'SERVICE': 'WMS',
96
+ 'VERSION': '1.3.0',
97
+ 'REQUEST': 'GetMap',
98
+ 'BBOX': f"{xmin},{ymin},{xmax},{ymax}",
99
+ 'CRS': 'EPSG:25832',
100
+ 'WIDTH': width,
101
+ 'HEIGHT': height,
102
+ 'LAYERS': "geodanmark_2023_12_5cm",
103
+ 'FORMAT': 'image/png',
104
+ 'STYLES': '',
105
+ 'DPI': 96,
106
+ 'MAP_RESOLUTION': 96,
107
+ 'FORMAT_OPTIONS': 'dpi:96'
108
+ }
109
+
110
+ # Down rgb image
111
+ base_url = "https://services.datafordeler.dk/GeoDanmarkOrto/orto_foraar/1.0.0/WMS"
112
+
113
+ try:
114
+ response = requests.get(base_url, params=wms_params)
115
+ response.raise_for_status()
116
+ except requests.exceptions.HTTPError as err:
117
+ print(err)
118
+ return None
119
+
120
+ img = Image.open(BytesIO(response.content)).convert("RGB")
121
+
122
+ # Download terrain
123
+ skygge_url = "https://services.datafordeler.dk/DHMNedboer/dhm/1.0.0/WMS"
124
+ wms_params["LAYERS"] = "dhm_terraen_skyggekort"
125
+
126
+ try:
127
+ response = requests.get(skygge_url, params=wms_params)
128
+ response.raise_for_status()
129
+ except requests.exceptions.HTTPError as err:
130
+ print(err)
131
+ return None
132
+
133
+ skygge_img = Image.open(BytesIO(response.content)).convert("L")
134
+
135
+ # Create mask
136
+ mask = Image.new('L', (width, height), 0)
137
+
138
+ # Convert coordinates to image space
139
+ x_norm = [(x - xmin) / roi_width for x in xs]
140
+ y_norm = [(y - ymin) / roi_height for y in ys]
141
+ x_img = [int(x * width) for x in x_norm]
142
+ y_img = [int((1 - y) * height) for y in y_norm]
143
+
144
+ # Draw polygon on mask
145
+ ImageDraw.Draw(mask).polygon(list(zip(x_img, y_img)), outline=255, fill=255)
146
+
147
+ array = np.concatenate([np.array(img),
148
+ np.array(skygge_img)[:, :, np.newaxis],
149
+ np.array(mask)[:, :, np.newaxis]],
150
+ axis=2)
151
+
152
+ return array
153
+
154
+
155
+ def predict(image, image_size):
156
+
157
+ image = pad_if_needed(image, image_size)
158
+ image = normalize_image(image)
159
+ image = np.transpose(image, (2, 0, 1))
160
+ image = image[np.newaxis]
161
+
162
+ input_names = model.get_inputs()[0].name
163
+ output_names = [output.name for output in model.get_outputs()]
164
+ ort_inputs = {input_names: image}
165
+ ort_outputs = model.run(None, ort_inputs)
166
+ predictions = {name: softmax(output) for name, output in zip(output_names, ort_outputs)}
167
+
168
+ return predictions
169
+
170
+ pretty_target_name = {
171
+ "hovednaturtype": "Hovednaturtype",
172
+ "arealet_nbl": "Paragraf 3",
173
+ "naturtilstand": "Naturtilstand",
174
+ }
175
+
176
+ def format_predictions(predictions):
177
+ result_list = []
178
+
179
+ for target, logits in predictions.items():
180
+ # Get the index of the highest probability
181
+ top_idx = np.argmax(logits[0])
182
+
183
+ # Get the probability value
184
+ confidence = float(logits[0][top_idx])
185
+
186
+ # Get the class name from idx_to_target mapping
187
+ class_name = idx_to_target[target][str(top_idx)]
188
+
189
+ if target != "naturtilstand":
190
+ class_name = class_name.capitalize()
191
+ else:
192
+ class_name = class_name.upper()
193
+
194
+
195
+ target_name = pretty_target_name[target]
196
+
197
+ # Format as concise HTML with class name and confidence percentage
198
+ html_result = f"<div>{target_name}: <i>{class_name}</i> ({confidence:.1%})</div>"
199
+ result_list.append(html_result)
200
+
201
+ return "".join(result_list)
202
+
203
+
204
+ class GeoJSONInput(BaseModel):
205
+ geojson: Dict[str, Any]
206
+
207
+ class ResultOutput(BaseModel):
208
+ result: str
209
+
210
+ @app.get("/", response_class=HTMLResponse)
211
+ async def get_html():
212
+ html_file = "index.html"
213
+ with open(html_file, "r") as f:
214
+ content = f.read()
215
+ return HTMLResponse(content=content)
216
+
217
+ @app.post("/predict")
218
+ async def predict_endpoint(geojson_input: GeoJSONInput) -> ResultOutput:
219
+ try:
220
+ coords = geojson_input.geojson['geometry']['coordinates'][0]
221
+ image = get_image(coords, IMAGE_SIZE)
222
+ predictions = predict(image, IMAGE_SIZE)
223
+ result = format_predictions(predictions)
224
+ return ResultOutput(result = result)
225
+
226
+ except Exception as e:
227
+ raise HTTPException(status_code=500, detail=str(e))
228
+
model.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96b1c612a6425460917fc754d93e76d2601523b6f7218e6c12d12fd19737a13e
3
+ size 513406756
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ numpy
4
+ onnxruntime
5
+ pydantic
6
+ cryptography
7
+ requests
8
+ pyproj
9
+ pillow