|
from fastapi import FastAPI, File, UploadFile |
|
from fastapi.responses import HTMLResponse |
|
from transformers import pipeline |
|
from PIL import Image |
|
import io |
|
import uvicorn |
|
import base64 |
|
|
|
app = FastAPI() |
|
|
|
|
|
def load_models(): |
|
return { |
|
"chest_classifier": pipeline("image-classification", model="codewithdark/vit-chest-xray") |
|
} |
|
|
|
models = load_models() |
|
|
|
|
|
LABEL_MAP = { |
|
'LABEL_0': 'Kardiomegalie', |
|
'LABEL_1': 'Ödem', |
|
'LABEL_2': 'Konsolidierung', |
|
'LABEL_3': 'Lungenentzündung', |
|
'LABEL_4': 'Kein Befund' |
|
} |
|
|
|
def translate_label(label): |
|
|
|
if isinstance(label, str) and label in LABEL_MAP: |
|
return LABEL_MAP[label] |
|
|
|
return f"Unbekannt ({label})" |
|
return translations.get(label, label) |
|
|
|
def image_to_base64(image): |
|
buffered = io.BytesIO() |
|
image.save(buffered, format="PNG") |
|
img_str = base64.b64encode(buffered.getvalue()).decode() |
|
return f"data:image/png;base64,{img_str}" |
|
|
|
COMMON_STYLES = """ |
|
body { |
|
font-family: system-ui, -apple-system, sans-serif; |
|
background: #f0f2f5; |
|
margin: 0; |
|
padding: 20px; |
|
color: #1a1a1a; |
|
} |
|
::-webkit-scrollbar { |
|
width: 8px; |
|
height: 8px; |
|
} |
|
|
|
::-webkit-scrollbar-track { |
|
background: transparent; |
|
} |
|
|
|
::-webkit-scrollbar-thumb { |
|
background-color: rgba(156, 163, 175, 0.5); |
|
border-radius: 4px; |
|
} |
|
|
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
background: white; |
|
padding: 20px; |
|
border-radius: 10px; |
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
} |
|
.button { |
|
background: #2d2d2d; |
|
color: white; |
|
border: none; |
|
padding: 12px 30px; |
|
border-radius: 8px; |
|
cursor: pointer; |
|
font-size: 1.1em; |
|
transition: all 0.3s ease; |
|
position: relative; |
|
} |
|
.button:hover { |
|
background: #404040; |
|
} |
|
@keyframes progress { |
|
0% { width: 0; } |
|
100% { width: 100%; } |
|
} |
|
.button-progress { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
height: 4px; |
|
background: rgba(255, 255, 255, 0.5); |
|
width: 0; |
|
} |
|
.button:active .button-progress { |
|
animation: progress 2s linear forwards; |
|
} |
|
img { |
|
max-width: 100%; |
|
height: auto; |
|
border-radius: 8px; |
|
} |
|
@keyframes blink { |
|
0% { opacity: 1; } |
|
50% { opacity: 0; } |
|
100% { opacity: 1; } |
|
} |
|
#loading { |
|
display: none; |
|
color: white; |
|
margin-top: 10px; |
|
animation: blink 1s infinite; |
|
text-align: center; |
|
} |
|
""" |
|
|
|
@app.get("/", response_class=HTMLResponse) |
|
async def main(): |
|
content = f""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Thorax-Röntgen Analyse</title> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<style> |
|
{COMMON_STYLES} |
|
|
|
.upload-section {{ |
|
background: #2d2d2d; |
|
padding: 40px; |
|
border-radius: 12px; |
|
margin: 20px 0; |
|
text-align: center; |
|
border: 2px dashed #404040; |
|
transition: all 0.3s ease; |
|
color: white; |
|
}} |
|
.upload-section:hover {{ |
|
border-color: #555; |
|
}} |
|
input[type="file"] {{ |
|
width: 0.1px; |
|
height: 0.1px; |
|
opacity: 0; |
|
overflow: hidden; |
|
position: absolute; |
|
z-index: -1; |
|
}} |
|
.file-upload-label {{ |
|
display: inline-block; |
|
padding: 12px 30px; |
|
background: #404040; |
|
color: white; |
|
border-radius: 8px; |
|
cursor: pointer; |
|
font-size: 1.1em; |
|
transition: all 0.3s ease; |
|
margin: 20px 0; |
|
}} |
|
.file-upload-label:hover {{ |
|
background: #555; |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="upload-section"> |
|
<form action="/analyze" method="post" enctype="multipart/form-data" onsubmit="document.getElementById('loading').style.display = 'block';"> |
|
<div> |
|
<label for="file-upload" class="file-upload-label"> |
|
Röntgenbild auswählen |
|
</label> |
|
<input id="file-upload" type="file" name="file" accept="image/*" required> |
|
</div> |
|
<button type="submit" class="button"> |
|
Analysieren |
|
<div class="button-progress"></div> |
|
</button> |
|
<div id="loading">Wird geladen...</div> |
|
</form> |
|
</div> |
|
</div> |
|
</body> |
|
</html> |
|
""" |
|
return content |
|
|
|
@app.post("/analyze", response_class=HTMLResponse) |
|
async def analyze_file(file: UploadFile = File(...)): |
|
try: |
|
contents = await file.read() |
|
image = Image.open(io.BytesIO(contents)) |
|
|
|
predictions = models["chest_classifier"](image) |
|
result_image_b64 = image_to_base64(image) |
|
|
|
results_html = f""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Ergebnisse</title> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<style> |
|
{COMMON_STYLES} |
|
|
|
.results-grid {{ |
|
display: grid; |
|
grid-template-columns: 1fr 1fr; |
|
gap: 20px; |
|
margin-top: 20px; |
|
}} |
|
.result-box {{ |
|
background: white; |
|
padding: 20px; |
|
border-radius: 12px; |
|
margin: 10px 0; |
|
border: 1px solid #e9ecef; |
|
}} |
|
.score-high {{ |
|
color: #0066cc; |
|
font-weight: bold; |
|
}} |
|
.score-medium {{ |
|
color: #ffa500; |
|
font-weight: bold; |
|
}} |
|
.back-button {{ |
|
display: inline-block; |
|
text-decoration: none; |
|
margin-top: 20px; |
|
}} |
|
h3 {{ |
|
color: #0066cc; |
|
margin-top: 0; |
|
}} |
|
@media (max-width: 768px) {{ |
|
.results-grid {{ |
|
grid-template-columns: 1fr; |
|
}} |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="results-grid"> |
|
<div class="result-box"> |
|
<h3>Analyse-Ergebnisse</h3> |
|
""" |
|
|
|
for pred in predictions: |
|
confidence_class = "score-high" if pred['score'] > 0.7 else "score-medium" |
|
results_html += f""" |
|
<div> |
|
<span class="{confidence_class}">{pred['score']:.1%}</span> - |
|
{translate_label(pred['label'])} |
|
</div> |
|
""" |
|
|
|
results_html += f""" |
|
</div> |
|
<div class="result-box"> |
|
<h3>Röntgenbild</h3> |
|
<img src="{result_image_b64}" alt="Analysiertes Röntgenbild"> |
|
</div> |
|
</div> |
|
|
|
<a href="/" class="button back-button"> |
|
← Zurück |
|
<div class="button-progress"></div> |
|
</a> |
|
</div> |
|
</body> |
|
</html> |
|
""" |
|
|
|
return results_html |
|
|
|
except Exception as e: |
|
return f""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Fehler</title> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<style> |
|
{COMMON_STYLES} |
|
.error-box {{ |
|
background: #fee2e2; |
|
border: 1px solid #ef4444; |
|
padding: 20px; |
|
border-radius: 8px; |
|
margin: 20px 0; |
|
}} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="error-box"> |
|
<h3>Fehler</h3> |
|
<p>{str(e)}</p> |
|
</div> |
|
<a href="/" class="button back-button"> |
|
← Zurück |
|
<div class="button-progress"></div> |
|
</a> |
|
</div> |
|
</body> |
|
</html> |
|
""" |
|
|
|
if __name__ == "__main__": |
|
uvicorn.run(app, host="0.0.0.0", port=7860) |