875 lines
28 KiB
HTML
875 lines
28 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Bone Quality Assessment - Анализ качества костной ткани</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
background: white;
|
||
border-radius: 24px;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
padding: 40px;
|
||
max-width: 1000px;
|
||
width: 100%;
|
||
}
|
||
|
||
h1 {
|
||
color: #2d3748;
|
||
font-size: 28px;
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.subtitle {
|
||
color: #718096;
|
||
font-size: 16px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 3px dashed #e2e8f0;
|
||
border-radius: 16px;
|
||
padding: 50px 20px;
|
||
text-align: center;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
background: #f7fafc;
|
||
position: relative;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #667eea;
|
||
background: #edf2f7;
|
||
}
|
||
|
||
.upload-area.drag-over {
|
||
border-color: #667eea;
|
||
background: #eef2ff;
|
||
transform: scale(1.02);
|
||
}
|
||
|
||
.upload-area .icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
display: block;
|
||
}
|
||
|
||
.upload-area h3 {
|
||
color: #2d3748;
|
||
font-size: 20px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.upload-area p {
|
||
color: #718096;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.upload-area input[type="file"] {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
opacity: 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
#previewContainer {
|
||
display: none;
|
||
margin-top: 24px;
|
||
padding: 20px;
|
||
background: #f7fafc;
|
||
border-radius: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
#previewContainer img {
|
||
max-width: 100%;
|
||
max-height: 400px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
#fileName {
|
||
margin-top: 12px;
|
||
color: #2d3748;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.btn-analyze {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
padding: 14px 40px;
|
||
border-radius: 12px;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
margin-top: 20px;
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-analyze:hover:not(:disabled) {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.btn-analyze:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn-analyze.loading {
|
||
position: relative;
|
||
color: transparent;
|
||
}
|
||
|
||
.btn-analyze.loading::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
margin: -12px 0 0 -12px;
|
||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||
border-top-color: white;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Results */
|
||
#results {
|
||
display: none;
|
||
margin-top: 30px;
|
||
animation: fadeIn 0.5s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.result-card {
|
||
background: #f7fafc;
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.result-card .label {
|
||
color: #718096;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.result-card .value {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #2d3748;
|
||
}
|
||
|
||
.quality-badge {
|
||
display: inline-block;
|
||
padding: 6px 20px;
|
||
border-radius: 20px;
|
||
font-weight: 700;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.quality-GOOD {
|
||
background: #c6f6d5;
|
||
color: #22543d;
|
||
}
|
||
|
||
.quality-WARNING {
|
||
background: #fefcbf;
|
||
color: #744210;
|
||
}
|
||
|
||
.quality-POOR {
|
||
background: #fed7d7;
|
||
color: #9b2c2c;
|
||
}
|
||
|
||
.severity-LOW {
|
||
background: #c6f6d5;
|
||
color: #22543d;
|
||
}
|
||
|
||
.severity-MEDIUM {
|
||
background: #fefcbf;
|
||
color: #744210;
|
||
}
|
||
|
||
.severity-HIGH {
|
||
background: #fed7d7;
|
||
color: #9b2c2c;
|
||
}
|
||
|
||
.issue-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.issue-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.issue-badge {
|
||
padding: 2px 12px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.issue-ARTIFACT {
|
||
background: #fefcbf;
|
||
color: #744210;
|
||
}
|
||
|
||
.issue-POSITION_ERROR {
|
||
background: #fed7d7;
|
||
color: #9b2c2c;
|
||
}
|
||
|
||
.issue-LOW_CONTRAST {
|
||
background: #bee3f8;
|
||
color: #2a69ac;
|
||
}
|
||
|
||
.grid-2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.validation-item {
|
||
background: white;
|
||
padding: 16px;
|
||
border-radius: 12px;
|
||
border-left: 4px solid #667eea;
|
||
}
|
||
|
||
.validation-item .label {
|
||
font-size: 13px;
|
||
color: #718096;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.validation-item .value {
|
||
font-size: 14px;
|
||
color: #2d3748;
|
||
margin-top: 4px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.validation-item .detail {
|
||
font-size: 12px;
|
||
color: #718096;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.confidence-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: #e2e8f0;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.confidence-bar .fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||
border-radius: 4px;
|
||
transition: width 0.8s ease;
|
||
}
|
||
|
||
.error-message {
|
||
background: #fed7d7;
|
||
color: #9b2c2c;
|
||
padding: 16px;
|
||
border-radius: 12px;
|
||
margin-top: 16px;
|
||
display: none;
|
||
}
|
||
|
||
.file-info {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
font-size: 14px;
|
||
color: #4a5568;
|
||
}
|
||
|
||
/* Mask visualization */
|
||
#maskContainer {
|
||
display: none;
|
||
margin-top: 24px;
|
||
padding: 20px;
|
||
background: #f7fafc;
|
||
border-radius: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
#maskContainer h3 {
|
||
color: #2d3748;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.mask-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 100%;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
border-radius: 8px;
|
||
background: #1a202c;
|
||
}
|
||
|
||
#maskImage {
|
||
display: block;
|
||
width: 100%;
|
||
height: auto;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
#maskCanvas {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.mask-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
margin: 16px 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mask-controls label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #4a5568;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.mask-controls input[type="range"] {
|
||
width: 150px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-toggle {
|
||
background: #edf2f7;
|
||
border: 1px solid #e2e8f0;
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
color: #4a5568;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn-toggle:hover {
|
||
background: #e2e8f0;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.btn-toggle.active {
|
||
background: #667eea;
|
||
color: white;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.mask-stats {
|
||
display: flex;
|
||
gap: 24px;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
font-size: 13px;
|
||
color: #718096;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.mask-stats span {
|
||
background: white;
|
||
padding: 4px 12px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.container {
|
||
padding: 20px;
|
||
}
|
||
.grid-2 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
h1 {
|
||
font-size: 22px;
|
||
}
|
||
.mask-controls {
|
||
flex-direction: column;
|
||
}
|
||
.mask-controls input[type="range"] {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1><a style="text-decoration: none;" href="/static/annotation.html">🦴</a> Bone Quality Assessment</h1>
|
||
<p class="subtitle">Загрузите медицинское изображение для анализа качества костной ткани</p>
|
||
|
||
<!-- Upload Area -->
|
||
<div class="upload-area" id="uploadArea">
|
||
<span class="icon">📤</span>
|
||
<h3>Перетащите изображение сюда</h3>
|
||
<p>или кликните для выбора файла</p>
|
||
<p style="font-size: 12px; color: #a0aec0; margin-top: 8px;">Поддерживаются: PNG, JPG, JPEG</p>
|
||
<input type="file" id="fileInput" accept=".png,.jpg,.jpeg">
|
||
</div>
|
||
|
||
<!-- Preview -->
|
||
<div id="previewContainer">
|
||
<img id="previewImage" src="#" alt="Preview">
|
||
<div id="fileName"></div>
|
||
<div class="file-info">
|
||
<span id="fileSize"></span>
|
||
<span>•</span>
|
||
<span id="fileType"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn-analyze" id="analyzeBtn" disabled>🔍 Анализировать</button>
|
||
|
||
<!-- Error -->
|
||
<div class="error-message" id="errorMessage"></div>
|
||
|
||
<!-- Mask Visualization -->
|
||
<div id="maskContainer">
|
||
<h3>🧩 Визуализация сегментации</h3>
|
||
<div class="mask-controls">
|
||
<label>
|
||
<span>Прозрачность:</span>
|
||
<input type="range" id="maskOpacity" min="0" max="100" value="50">
|
||
<span id="opacityLabel">50%</span>
|
||
</label>
|
||
<button class="btn-toggle active" id="toggleMaskBtn">Скрыть маску</button>
|
||
<button class="btn-toggle" id="toggleOriginalBtn">Показать оригинал</button>
|
||
</div>
|
||
<div class="mask-wrapper">
|
||
<img id="maskImage" src="#" alt="Mask Visualization">
|
||
<canvas id="maskCanvas"></canvas>
|
||
</div>
|
||
<div class="mask-stats" id="maskStats">
|
||
<span>🧊 Объектов: <strong id="numObjects">-</strong></span>
|
||
<span>📏 Размер: <strong id="objectSize">-</strong></span>
|
||
<span>🎯 Уверенность: <strong id="maskConfidence">-</strong></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results -->
|
||
<div id="results">
|
||
<div class="result-card">
|
||
<div class="label">Общее качество</div>
|
||
<div id="overallQuality"></div>
|
||
</div>
|
||
|
||
<div class="result-card">
|
||
<div class="label">Серьезность проблемы</div>
|
||
<div id="severityDisplay"></div>
|
||
</div>
|
||
|
||
<div class="result-card">
|
||
<div class="label">Выявленные проблемы</div>
|
||
<div id="issuesList"></div>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<div class="validation-item">
|
||
<div class="label">📍 Позиционирование</div>
|
||
<div class="value" id="positionValidation"></div>
|
||
<div class="detail" id="positionDetails"></div>
|
||
</div>
|
||
<div class="validation-item">
|
||
<div class="label">🔬 Артефакты</div>
|
||
<div class="value" id="artifactValidation"></div>
|
||
<div class="detail" id="artifactDetails"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="result-card">
|
||
<div class="label">Уверенность модели</div>
|
||
<div id="confidenceDisplay"></div>
|
||
<div class="confidence-bar">
|
||
<div class="fill" id="confidenceFill" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="result-card" id="metricsCard" style="display: none;">
|
||
<div class="label">Детальные метрики</div>
|
||
<div id="metricsDisplay" style="font-size: 14px; color: #4a5568; max-height: 300px; overflow-y: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Конфигурация
|
||
const API_URL = 'http://localhost:8000/api/v1/analyze';
|
||
|
||
// DOM элементы
|
||
const uploadArea = document.getElementById('uploadArea');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const previewContainer = document.getElementById('previewContainer');
|
||
const previewImage = document.getElementById('previewImage');
|
||
const fileName = document.getElementById('fileName');
|
||
const fileSize = document.getElementById('fileSize');
|
||
const fileType = document.getElementById('fileType');
|
||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||
const results = document.getElementById('results');
|
||
const errorMessage = document.getElementById('errorMessage');
|
||
const maskContainer = document.getElementById('maskContainer');
|
||
const maskImage = document.getElementById('maskImage');
|
||
const maskCanvas = document.getElementById('maskCanvas');
|
||
const maskOpacity = document.getElementById('maskOpacity');
|
||
const opacityLabel = document.getElementById('opacityLabel');
|
||
const toggleMaskBtn = document.getElementById('toggleMaskBtn');
|
||
const toggleOriginalBtn = document.getElementById('toggleOriginalBtn');
|
||
const numObjects = document.getElementById('numObjects');
|
||
const objectSize = document.getElementById('objectSize');
|
||
const maskConfidence = document.getElementById('maskConfidence');
|
||
|
||
// Состояние
|
||
let selectedFile = null;
|
||
let currentImageData = null;
|
||
let currentMaskData = null;
|
||
let showMask = true;
|
||
let currentMaskBase64 = null;
|
||
|
||
// Drag and drop
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('drag-over');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('drag-over');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('drag-over');
|
||
if (e.dataTransfer.files.length) {
|
||
handleFile(e.dataTransfer.files[0]);
|
||
}
|
||
});
|
||
|
||
fileInput.addEventListener('change', (e) => {
|
||
if (e.target.files.length) {
|
||
handleFile(e.target.files[0]);
|
||
}
|
||
});
|
||
|
||
function handleFile(file) {
|
||
selectedFile = file;
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
currentImageData = e.target.result;
|
||
previewImage.src = e.target.result;
|
||
previewContainer.style.display = 'block';
|
||
fileName.textContent = file.name;
|
||
fileSize.textContent = formatFileSize(file.size);
|
||
fileType.textContent = file.type || 'Неизвестно';
|
||
analyzeBtn.disabled = false;
|
||
results.style.display = 'none';
|
||
maskContainer.style.display = 'none';
|
||
errorMessage.style.display = 'none';
|
||
currentMaskData = null;
|
||
currentMaskBase64 = null;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||
}
|
||
|
||
// Mask controls
|
||
maskOpacity.addEventListener('input', () => {
|
||
const val = maskOpacity.value;
|
||
opacityLabel.textContent = val + '%';
|
||
if (currentImageData && currentMaskBase64) {
|
||
drawMask(currentImageData, currentMaskBase64, val / 100);
|
||
}
|
||
});
|
||
|
||
toggleMaskBtn.addEventListener('click', () => {
|
||
showMask = !showMask;
|
||
toggleMaskBtn.textContent = showMask ? 'Скрыть маску' : 'Показать маску';
|
||
toggleMaskBtn.classList.toggle('active');
|
||
if (currentImageData && currentMaskBase64) {
|
||
drawMask(currentImageData, currentMaskBase64, maskOpacity.value / 100);
|
||
}
|
||
});
|
||
|
||
toggleOriginalBtn.addEventListener('click', () => {
|
||
if (currentImageData) {
|
||
const ctx = maskCanvas.getContext('2d');
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
maskCanvas.width = img.width;
|
||
maskCanvas.height = img.height;
|
||
ctx.drawImage(img, 0, 0);
|
||
};
|
||
img.src = currentImageData;
|
||
}
|
||
toggleMaskBtn.textContent = 'Показать маску';
|
||
toggleMaskBtn.classList.remove('active');
|
||
showMask = false;
|
||
});
|
||
|
||
// Draw mask function
|
||
function drawMask(imageSrc, maskBase64, opacity) {
|
||
const ctx = maskCanvas.getContext('2d');
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
maskCanvas.width = img.width;
|
||
maskCanvas.height = img.height;
|
||
ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
if (showMask && maskBase64) {
|
||
const maskImg = new Image();
|
||
maskImg.onload = () => {
|
||
ctx.globalAlpha = opacity * 0.6;
|
||
ctx.drawImage(maskImg, 0, 0, maskCanvas.width, maskCanvas.height);
|
||
ctx.globalAlpha = 1;
|
||
|
||
// Информация о маске
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
||
ctx.fillRect(10, 10, 200, 60);
|
||
ctx.fillStyle = '#2d3748';
|
||
ctx.font = '12px Arial';
|
||
ctx.fillText(`Объектов: ${currentMaskData?.num_objects || 0}`, 20, 30);
|
||
ctx.fillText(`Размер: ${((currentMaskData?.object_size || 0) * 100).toFixed(1)}%`, 20, 50);
|
||
ctx.globalAlpha = 1;
|
||
};
|
||
maskImg.src = `data:image/png;base64,${maskBase64}`;
|
||
}
|
||
};
|
||
img.src = imageSrc;
|
||
}
|
||
|
||
// Analyze
|
||
analyzeBtn.addEventListener('click', async () => {
|
||
if (!selectedFile) return;
|
||
|
||
analyzeBtn.disabled = true;
|
||
analyzeBtn.classList.add('loading');
|
||
results.style.display = 'none';
|
||
maskContainer.style.display = 'none';
|
||
errorMessage.style.display = 'none';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', selectedFile);
|
||
|
||
try {
|
||
const response = await fetch(API_URL, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
throw new Error(`Ошибка сервера (${response.status}): ${errorText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// Сохраняем маску
|
||
if (data.mask) {
|
||
currentMaskBase64 = data.mask;
|
||
currentMaskData = {
|
||
num_objects: data.metrics?.artifact?.num_objects || 0,
|
||
object_size: data.metrics?.artifact?.object_size || 0,
|
||
edge_energy: data.metrics?.artifact?.edge_energy || 0
|
||
};
|
||
|
||
maskContainer.style.display = 'block';
|
||
maskImage.src = currentImageData;
|
||
|
||
// Обновляем статистику
|
||
numObjects.textContent = currentMaskData.num_objects;
|
||
objectSize.textContent = (currentMaskData.object_size * 100).toFixed(1) + '%';
|
||
maskConfidence.textContent = (data.confidence * 100).toFixed(1) + '%';
|
||
|
||
// Рисуем маску
|
||
drawMask(currentImageData, data.mask, maskOpacity.value / 100);
|
||
}
|
||
|
||
displayResults(data);
|
||
|
||
} catch (error) {
|
||
errorMessage.textContent = `❌ Ошибка: ${error.message}`;
|
||
errorMessage.style.display = 'block';
|
||
} finally {
|
||
analyzeBtn.disabled = false;
|
||
analyzeBtn.classList.remove('loading');
|
||
}
|
||
});
|
||
|
||
function displayResults(data) {
|
||
// Overall quality
|
||
const quality = data.overall_quality || 'GOOD';
|
||
const qualityEl = document.getElementById('overallQuality');
|
||
const badgeClass = `quality-${quality}`;
|
||
const labelMap = {
|
||
'GOOD': '✅ Хорошее',
|
||
'WARNING': '⚠️ Требует внимания',
|
||
'POOR': '❌ Плохое'
|
||
};
|
||
qualityEl.innerHTML = `<span class="quality-badge ${badgeClass}">${labelMap[quality] || quality}</span>`;
|
||
|
||
// Severity
|
||
const severity = data.severity || 'LOW';
|
||
const severityEl = document.getElementById('severityDisplay');
|
||
const severityClass = `severity-${severity}`;
|
||
const severityMap = {
|
||
'LOW': '🟢 Низкая',
|
||
'MEDIUM': '🟡 Средняя',
|
||
'HIGH': '🔴 Высокая'
|
||
};
|
||
severityEl.innerHTML = `<span class="quality-badge ${severityClass}">${severityMap[severity] || severity}</span>`;
|
||
|
||
// Issues
|
||
const issuesEl = document.getElementById('issuesList');
|
||
if (data.issues && data.issues.length > 0) {
|
||
issuesEl.innerHTML = data.issues.map(issue => {
|
||
const type = issue.type || 'info';
|
||
const badgeClass = `issue-${type}`;
|
||
const details = issue.details || '';
|
||
const severity = issue.severity || '';
|
||
return `<div class="issue-item">
|
||
<span class="issue-badge ${badgeClass}">${type.replace('_', ' ')}</span>
|
||
<span>${details}</span>
|
||
${severity ? `<span style="font-size: 12px; color: #718096;">(${severity})</span>` : ''}
|
||
</div>`;
|
||
}).join('');
|
||
} else {
|
||
issuesEl.innerHTML = '<div style="color: #48bb78;">✅ Проблем не обнаружено</div>';
|
||
}
|
||
|
||
// Position validation
|
||
const pos = data.position_validation || {};
|
||
const posEl = document.getElementById('positionValidation');
|
||
const posDetails = document.getElementById('positionDetails');
|
||
|
||
if (pos.valid !== undefined) {
|
||
posEl.innerHTML = pos.valid ? '✅ Корректное' : '❌ Некорректное';
|
||
posDetails.textContent = pos.position ? `Позиция: ${pos.position}, отклонение: ${(pos.deviation || 0).toFixed(3)}` : '';
|
||
} else {
|
||
posEl.textContent = 'Не определено';
|
||
posDetails.textContent = '';
|
||
}
|
||
|
||
// Artifact validation
|
||
const art = data.artifact_validation || {};
|
||
const artEl = document.getElementById('artifactValidation');
|
||
const artDetails = document.getElementById('artifactDetails');
|
||
|
||
if (art.artifact !== undefined) {
|
||
artEl.innerHTML = art.artifact ? '⚠️ Обнаружены' : '✅ Не обнаружены';
|
||
artDetails.textContent = art.type ? `Тип: ${art.type}` : '';
|
||
} else {
|
||
artEl.textContent = 'Не определено';
|
||
artDetails.textContent = '';
|
||
}
|
||
|
||
// Confidence
|
||
const confidence = data.confidence || 0;
|
||
document.getElementById('confidenceDisplay').textContent =
|
||
`${(confidence * 100).toFixed(1)}%`;
|
||
document.getElementById('confidenceFill').style.width = `${(confidence * 100).toFixed(1)}%`;
|
||
|
||
// Metrics
|
||
const metrics = data.metrics || {};
|
||
const metricsCard = document.getElementById('metricsCard');
|
||
const metricsDisplay = document.getElementById('metricsDisplay');
|
||
|
||
if (Object.keys(metrics).length > 0) {
|
||
metricsCard.style.display = 'block';
|
||
metricsDisplay.innerHTML = `<pre style="white-space: pre-wrap; word-break: break-all; font-size: 12px;">${JSON.stringify(metrics, null, 2)}</pre>`;
|
||
} else {
|
||
metricsCard.style.display = 'none';
|
||
}
|
||
|
||
results.style.display = 'block';
|
||
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
|
||
// Health check
|
||
async function checkHealth() {
|
||
try {
|
||
const response = await fetch('http://localhost:8000/api/v1/health');
|
||
if (response.ok) {
|
||
console.log('✅ API сервер работает');
|
||
} else {
|
||
console.warn('⚠️ API сервер недоступен');
|
||
}
|
||
} catch {
|
||
console.warn('⚠️ Не удалось подключиться к API серверу');
|
||
}
|
||
}
|
||
checkHealth();
|
||
|
||
console.log('🦴 Bone Quality Assessment UI загружен');
|
||
console.log(`API URL: ${API_URL}`);
|
||
</script>
|
||
</body>
|
||
</html> |