ftx7go commited on
Commit
e6314ee
·
verified ·
1 Parent(s): 7b6b4dd

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +403 -0
app.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile
2
+ from fastapi.responses import HTMLResponse
3
+ from transformers import pipeline
4
+ from PIL import Image, ImageDraw
5
+ import numpy as np
6
+ import io
7
+ import uvicorn
8
+ import base64
9
+
10
+ app = FastAPI()
11
+
12
+ # Chargement des modèles
13
+ def load_models():
14
+ return {
15
+ "KnochenAuge": pipeline("object-detection", model="D3STRON/bone-fracture-detr"),
16
+ "KnochenWächter": pipeline("image-classification", model="Heem2/bone-fracture-detection-using-xray"),
17
+ "RöntgenMeister": pipeline("image-classification",
18
+ model="nandodeomkar/autotrain-fracture-detection-using-google-vit-base-patch-16-54382127388")
19
+ }
20
+
21
+ models = load_models()
22
+
23
+ def translate_label(label):
24
+ translations = {
25
+ "fracture": "Knochenbruch",
26
+ "no fracture": "Kein Knochenbruch",
27
+ "normal": "Normal",
28
+ "abnormal": "Auffällig",
29
+ "F1": "Knochenbruch",
30
+ "NF": "Kein Knochenbruch"
31
+ }
32
+ return translations.get(label.lower(), label)
33
+
34
+ def create_heatmap_overlay(image, box, score):
35
+ overlay = Image.new('RGBA', image.size, (0, 0, 0, 0))
36
+ draw = ImageDraw.Draw(overlay)
37
+
38
+ x1, y1 = box['xmin'], box['ymin']
39
+ x2, y2 = box['xmax'], box['ymax']
40
+
41
+ if score > 0.8:
42
+ fill_color = (255, 0, 0, 100)
43
+ border_color = (255, 0, 0, 255)
44
+ elif score > 0.6:
45
+ fill_color = (255, 165, 0, 100)
46
+ border_color = (255, 165, 0, 255)
47
+ else:
48
+ fill_color = (255, 255, 0, 100)
49
+ border_color = (255, 255, 0, 255)
50
+
51
+ draw.rectangle([x1, y1, x2, y2], fill=fill_color)
52
+ draw.rectangle([x1, y1, x2, y2], outline=border_color, width=2)
53
+
54
+ return overlay
55
+
56
+ def draw_boxes(image, predictions):
57
+ result_image = image.copy().convert('RGBA')
58
+
59
+ for pred in predictions:
60
+ box = pred['box']
61
+ score = pred['score']
62
+
63
+ overlay = create_heatmap_overlay(image, box, score)
64
+ result_image = Image.alpha_composite(result_image, overlay)
65
+
66
+ draw = ImageDraw.Draw(result_image)
67
+ temp = 36.5 + (score * 2.5)
68
+ label = f"{translate_label(pred['label'])} ({score:.1%} • {temp:.1f}°C)"
69
+
70
+ text_bbox = draw.textbbox((box['xmin'], box['ymin']-20), label)
71
+ draw.rectangle(text_bbox, fill=(0, 0, 0, 180))
72
+
73
+ draw.text(
74
+ (box['xmin'], box['ymin']-20),
75
+ label,
76
+ fill=(255, 255, 255, 255)
77
+ )
78
+
79
+ return result_image
80
+
81
+ def image_to_base64(image):
82
+ buffered = io.BytesIO()
83
+ image.save(buffered, format="PNG")
84
+ img_str = base64.b64encode(buffered.getvalue()).decode()
85
+ return f"data:image/png;base64,{img_str}"
86
+
87
+ COMMON_STYLES = """
88
+ body {
89
+ font-family: system-ui, -apple-system, sans-serif;
90
+ background: #f0f2f5;
91
+ margin: 0;
92
+ padding: 20px;
93
+ color: #1a1a1a;
94
+ }
95
+ ::-webkit-scrollbar {
96
+ width: 8px;
97
+ height: 8px;
98
+ }
99
+
100
+ ::-webkit-scrollbar-track {
101
+ background: transparent;
102
+ }
103
+
104
+ ::-webkit-scrollbar-thumb {
105
+ background-color: rgba(156, 163, 175, 0.5);
106
+ border-radius: 4px;
107
+ }
108
+
109
+ .container {
110
+ max-width: 1200px;
111
+ margin: 0 auto;
112
+ background: white;
113
+ padding: 20px;
114
+ border-radius: 10px;
115
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
116
+ }
117
+ .button {
118
+ background: #2d2d2d;
119
+ color: white;
120
+ border: none;
121
+ padding: 12px 30px;
122
+ border-radius: 8px;
123
+ cursor: pointer;
124
+ font-size: 1.1em;
125
+ transition: all 0.3s ease;
126
+ position: relative;
127
+ }
128
+ .button:hover {
129
+ background: #404040;
130
+ }
131
+ @keyframes progress {
132
+ 0% { width: 0; }
133
+ 100% { width: 100%; }
134
+ }
135
+ .button-progress {
136
+ position: absolute;
137
+ bottom: 0;
138
+ left: 0;
139
+ height: 4px;
140
+ background: rgba(255, 255, 255, 0.5);
141
+ width: 0;
142
+ }
143
+ .button:active .button-progress {
144
+ animation: progress 2s linear forwards;
145
+ }
146
+ img {
147
+ max-width: 100%;
148
+ height: auto;
149
+ border-radius: 8px;
150
+ }
151
+ @keyframes blink {
152
+ 0% { opacity: 1; }
153
+ 50% { opacity: 0; }
154
+ 100% { opacity: 1; }
155
+ }
156
+ #loading {
157
+ display: none;
158
+ color: white;
159
+ margin-top: 10px;
160
+ animation: blink 1s infinite;
161
+ text-align: center;
162
+ }
163
+ """
164
+
165
+ @app.get("/", response_class=HTMLResponse)
166
+ async def main():
167
+ content = f"""
168
+ <!DOCTYPE html>
169
+ <html>
170
+ <head>
171
+ <title>Fraktur Detektion</title>
172
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
173
+ <style>
174
+ {COMMON_STYLES}
175
+
176
+ .upload-section {{
177
+ background: #2d2d2d;
178
+ padding: 40px;
179
+ border-radius: 12px;
180
+ margin: 20px 0;
181
+ text-align: center;
182
+ border: 2px dashed #404040;
183
+ transition: all 0.3s ease;
184
+ color: white;
185
+ }}
186
+ .upload-section:hover {{
187
+ border-color: #555;
188
+ }}
189
+ input[type="file"] {{
190
+ font-size: 1.1em;
191
+ margin: 20px 0;
192
+ color: white;
193
+ }}
194
+ input[type="file"]::file-selector-button {{
195
+ font-size: 1em;
196
+ padding: 10px 20px;
197
+ border-radius: 8px;
198
+ border: 1px solid #404040;
199
+ background: #2d2d2d;
200
+ color: white;
201
+ transition: all 0.3s ease;
202
+ cursor: pointer;
203
+ }}
204
+ input[type="file"]::file-selector-button:hover {{
205
+ background: #404040;
206
+ }}
207
+ .confidence-slider {{
208
+ width: 100%;
209
+ max-width: 300px;
210
+ margin: 20px auto;
211
+ }}
212
+ input[type="range"] {{
213
+ width: 100%;
214
+ height: 8px;
215
+ border-radius: 4px;
216
+ background: #404040;
217
+ outline: none;
218
+ transition: all 0.3s ease;
219
+ -webkit-appearance: none;
220
+ }}
221
+ input[type="range"]::-webkit-slider-thumb {{
222
+ -webkit-appearance: none;
223
+ width: 20px;
224
+ height: 20px;
225
+ border-radius: 50%;
226
+ background: white;
227
+ cursor: pointer;
228
+ border: none;
229
+ }}
230
+ </style>
231
+ </head>
232
+ <body>
233
+ <div class="container">
234
+ <div class="upload-section">
235
+ <form action="/analyze" method="post" enctype="multipart/form-data" onsubmit="document.getElementById('loading').style.display = 'block';">
236
+ <div>
237
+ <input type="file" name="file" accept="image/*" required>
238
+ </div>
239
+ <div class="confidence-slider">
240
+ <label for="threshold">Konfidenzschwelle: <span id="thresholdValue">0.60</span></label>
241
+ <input type="range" id="threshold" name="threshold"
242
+ min="0" max="1" step="0.05" value="0.60"
243
+ oninput="document.getElementById('thresholdValue').textContent = parseFloat(this.value).toFixed(2)">
244
+ </div>
245
+ <button type="submit" class="button">
246
+ Analysieren
247
+ <div class="button-progress"></div>
248
+ </button>
249
+ <div id="loading">Loading...</div>
250
+ </form>
251
+ </div>
252
+ </div>
253
+ </body>
254
+ </html>
255
+ """
256
+ return content
257
+
258
+ @app.post("/analyze", response_class=HTMLResponse)
259
+ async def analyze_file(file: UploadFile = File(...)):
260
+ try:
261
+ contents = await file.read()
262
+ image = Image.open(io.BytesIO(contents))
263
+
264
+ predictions_watcher = models["KnochenWächter"](image)
265
+ predictions_master = models["RöntgenMeister"](image)
266
+ predictions_locator = models["KnochenAuge"](image)
267
+
268
+ filtered_preds = [p for p in predictions_locator if p['score'] >= 0.6]
269
+ if filtered_preds:
270
+ result_image = draw_boxes(image, filtered_preds)
271
+ else:
272
+ result_image = image
273
+
274
+ result_image_b64 = image_to_base64(result_image)
275
+
276
+ results_html = f"""
277
+ <!DOCTYPE html>
278
+ <html>
279
+ <head>
280
+ <title>Ergebnisse</title>
281
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
282
+ <style>
283
+ {COMMON_STYLES}
284
+
285
+ .results-grid {{
286
+ display: grid;
287
+ grid-template-columns: 1fr 1fr;
288
+ gap: 20px;
289
+ margin-top: 20px;
290
+ }}
291
+ .result-box {{
292
+ background: white;
293
+ padding: 20px;
294
+ border-radius: 12px;
295
+ margin: 10px 0;
296
+ border: 1px solid #e9ecef;
297
+ }}
298
+ .score-high {{
299
+ color: #0066cc;
300
+ font-weight: bold;
301
+ }}
302
+
303
+ .score-medium {{
304
+ color: #ffa500;
305
+ font-weight: bold;
306
+ }}
307
+ .back-button {{
308
+ display: inline-block;
309
+ text-decoration: none;
310
+ margin-top: 20px;
311
+ }}
312
+ h3 {{
313
+ color: #0066cc;
314
+ margin-top: 0;
315
+ }}
316
+ @media (max-width: 768px) {{
317
+ .results-grid {{
318
+ grid-template-columns: 1fr;
319
+ }}
320
+ }}
321
+ </style>
322
+ </head>
323
+ <body>
324
+ <div class="container">
325
+ <div class="results-grid">
326
+ <div>
327
+ <div class="result-box"><h3>KnochenWächter</h3>
328
+ """
329
+
330
+ for pred in predictions_watcher:
331
+ confidence_class = "score-high" if pred['score'] > 0.7 else "score-medium"
332
+ results_html += f"""
333
+ <div>
334
+ <span class="{confidence_class}">{pred['score']:.1%}</span> -
335
+ {translate_label(pred['label'])}
336
+ </div>
337
+ """
338
+ results_html += "</div>"
339
+
340
+ results_html += "<div class='result-box'><h3>RöntgenMeister</h3>"
341
+ for pred in predictions_master:
342
+ confidence_class = "score-high" if pred['score'] > 0.7 else "score-medium"
343
+ results_html += f"""
344
+ <div>
345
+ <span class="{confidence_class}">{pred['score']:.1%}</span> -
346
+ {translate_label(pred['label'])}
347
+ </div>
348
+ """
349
+ results_html += "</div></div>"
350
+
351
+ results_html += f"""
352
+ <div class='result-box'>
353
+ <h3>Fraktur Lokalisation</h3>
354
+ <img src="{result_image_b64}" alt="Analyzed image">
355
+ </div>
356
+ </div>
357
+
358
+ <a href="/" class="button back-button">
359
+ ← Zurück
360
+ <div class="button-progress"></div>
361
+ </a>
362
+ </div>
363
+ </body>
364
+ </html>
365
+ """
366
+
367
+ return results_html
368
+
369
+ except Exception as e:
370
+ return f"""
371
+ <!DOCTYPE html>
372
+ <html>
373
+ <head>
374
+ <title>Fehler</title>
375
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
376
+ <style>
377
+ {COMMON_STYLES}
378
+ .error-box {{
379
+ background: #fee2e2;
380
+ border: 1px solid #ef4444;
381
+ padding: 20px;
382
+ border-radius: 8px;
383
+ margin: 20px 0;
384
+ }}
385
+ </style>
386
+ </head>
387
+ <body>
388
+ <div class="container">
389
+ <div class="error-box">
390
+ <h3>Fehler</h3>
391
+ <p>{str(e)}</p>
392
+ </div>
393
+ <a href="/" class="button back-button">
394
+ ← Zurück
395
+ <div class="button-progress"></div>
396
+ </a>
397
+ </div>
398
+ </body>
399
+ </html>
400
+ """
401
+
402
+ if __name__ == "__main__":
403
+ uvicorn.run(app, host="0.0.0.0", port=7860)