feat(frontend): 前端样式优化

This commit is contained in:
ClovertaTheTrilobita 2025-10-17 12:41:34 +00:00
parent 723bfdf2d9
commit 2089efb1cb
4 changed files with 437 additions and 163 deletions

View file

@ -140,22 +140,47 @@ def run_upload():
@app.post("/train_upload") @app.post("/train_upload")
def train_upload(): def train_upload():
def _to_float(x, default):
try:
return float(x)
except (TypeError, ValueError):
return default
def _to_int(x, default):
try:
return int(x)
except (TypeError, ValueError):
return default
ts = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + f"-{int(time.time()*1000)%1000:03d}" ts = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + f"-{int(time.time()*1000)%1000:03d}"
model_name = request.args.get("model_name") or f"custom_model-{ts}" model_name = request.args.get("model_name") or f"custom_model-{ts}"
image_filter = request.args.get("image_filter") or "_img" image_filter = request.args.get("image_filter") or "_img"
mask_filter = request.args.get("mask_filter") or "_masks" mask_filter = request.args.get("mask_filter") or "_masks"
base_model = request.args.get("base_model") or "cpsam" base_model = request.args.get("base_model") or "cpsam"
batch_size = request.args.get("batch_size") or 8 batch_size = _to_int(request.args.get("batch_size"), 8)
learning_rate = request.args.get("learning_rate") or 5e-5 learning_rate = _to_float(request.args.get("learning_rate"), 5e-5)
n_epochs = request.args.get("n_epochs") or 100 n_epochs = _to_int(request.args.get("n_epochs"), 100)
weight_decay = request.args.get("weight_decay") or 0.1 weight_decay = _to_float(request.args.get("weight_decay"), 0.1)
normalize = request.args.get("normalize") or True normalize = request.args.get(
compute_flows = request.args.get("compute_flows") or False "normalize",
min_train_masks = request.args.get(" min_train_masks") or 5 default=True,
nimg_per_epoch = request.args.get("nimg_per_epoch") or None type=lambda v: str(v).strip().lower() in ("1","true","t","yes","y","on")
rescale = request.args.get("rescale") or False )
scale_range = request.args.get("scale_range") or None compute_flows = request.args.get(
channel_axis = request.args.get("channel_axis") or None "compute_flows",
default=True,
type=lambda v: str(v).strip().lower() in ("1","true","t","yes","y","on")
)
min_train_masks = _to_int(request.args.get(" min_train_masks"), 5)
nimg_per_epoch = _to_int(request.args.get("nimg_per_epoch"), None)
rescale = request.args.get(
"rescale",
default=False,
type=lambda v: str(v).strip().lower() in ("1","true","t","yes","y","on")
)
scale_range = _to_float(request.args.get("scale_range"), None)
channel_axis = _to_int(request.args.get("channel_axis"), None)
train_files = request.files.getlist("train_files") train_files = request.files.getlist("train_files")
test_files = request.files.getlist("test_files") test_files = request.files.getlist("test_files")

View file

@ -7,6 +7,7 @@
<title>Bootstrap demo</title> <title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous"> integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
</head> </head>
<body> <body>

View file

@ -12,54 +12,82 @@
<style> <style>
/* 背景与玻璃卡片 —— 与首页/预览页一致 */ /* 背景与玻璃卡片 —— 与首页/预览页一致 */
body{ body {
min-height:100vh; min-height: 100vh;
background: radial-gradient(1200px 600px at 20% 10%, rgba(13,110,253,.15), transparent 60%), background: radial-gradient(1200px 600px at 20% 10%, rgba(13,110,253,.15), transparent 60%),
radial-gradient(800px 400px at 80% 90%, rgba(32,201,151,.18), transparent 60%), radial-gradient(800px 400px at 80% 90%, rgba(32,201,151,.18), transparent 60%),
linear-gradient(180deg, #f8f9fa, #eef2f7); linear-gradient(180deg, #f8f9fa, #eef2f7);
} }
@media (prefers-color-scheme: dark){ @media (prefers-color-scheme: dark) {
body{ body {
background: radial-gradient(1200px 600px at 20% 10%, rgba(13,110,253,.25), transparent 60%), background: radial-gradient(1200px 600px at 20% 10%, rgba(13,110,253,.25), transparent 60%),
radial-gradient(800px 400px at 80% 90%, rgba(32,201,151,.25), transparent 60%), radial-gradient(800px 400px at 80% 90%, rgba(32,201,151,.25), transparent 60%),
linear-gradient(180deg, #0b1020, #0e1326); linear-gradient(180deg, #0b1020, #0e1326);
} }
} }
.glass-card{ .glass-card {
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
background: rgba(255,255,255,.65); background: rgba(255,255,255,.65);
border:1px solid rgba(255,255,255,.5); border: 1px solid rgba(255,255,255,.5);
} }
[data-bs-theme="dark"] .glass-card{ [data-bs-theme="dark"] .glass-card {
background: rgba(17,20,34,.6); background: rgba(17,20,34,.6);
border:1px solid rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.08);
}
.section-title {
display: flex; align-items: center; gap: .6rem; margin-bottom: 0;
}
.hint { font-size: .9rem; opacity: .75 }
/* 小卡片(参数分组) */
.subcard {
background: rgba(255,255,255,.6);
border: 1px solid rgba(0,0,0,.05);
border-radius: 1rem;
padding: 1rem;
}
[data-bs-theme="dark"] .subcard {
background: rgba(255,255,255,.04);
border-color: rgba(255,255,255,.08);
} }
.section-title{ /* input-group 统一图标透明度 */
display:flex; align-items:center; gap:.6rem; .input-group-text i { opacity: .8; }
margin-bottom:0;
}
.hint{font-size:.9rem; opacity:.75}
/* 原生 progress 美化(保持 id 不变) */ /* progress 美化(保持 id 不变) */
#bar{ #bar {
width:100%; height:1rem; border:none; border-radius:.75rem; width: 100%;
background-color:rgba(13,110,253,.15); overflow:hidden; vertical-align:middle; height: 1rem;
border: none;
border-radius: .75rem;
background-color: rgba(13,110,253,.15);
overflow: hidden;
vertical-align: middle;
}
#bar::-webkit-progress-bar { background: transparent; }
#bar::-webkit-progress-value {
background: linear-gradient(90deg, #0d6efd, #6ea8fe);
border-radius: .75rem;
}
#bar::-moz-progress-bar {
background: linear-gradient(90deg, #0d6efd, #6ea8fe);
border-radius: .75rem;
} }
#bar::-webkit-progress-bar{background:transparent}
#bar::-webkit-progress-value{background:linear-gradient(90deg,#0d6efd,#6ea8fe); border-radius:.75rem}
#bar::-moz-progress-bar{background:linear-gradient(90deg,#0d6efd,#6ea8fe); border-radius:.75rem}
/* 上传按钮动效(保持 id 不变) */ /* 上传/启动按钮动效(保持 id 不变) */
#uploadBtn{ #uploadBtn {
padding:.65rem 1.1rem; border-radius:.8rem; padding: .65rem 1.1rem;
transition:transform .12s ease, box-shadow .12s ease; border-radius: .8rem;
transition: transform .12s ease, box-shadow .12s ease;
} }
#uploadBtn:hover, #uploadBtn:focus-visible{ #uploadBtn:hover, #uploadBtn:focus-visible {
transform:translateY(-1px); transform: translateY(-1px);
box-shadow:0 .75rem 1.25rem rgba(0,0,0,.12); box-shadow: 0 .75rem 1.25rem rgba(0,0,0,.12);
} }
/* 更紧凑的行距 */
.g-tight { row-gap: .75rem; }
</style> </style>
</head> </head>
@ -68,7 +96,9 @@
<div class="glass-card rounded-4 shadow-lg p-4 p-md-5"> <div class="glass-card rounded-4 shadow-lg p-4 p-md-5">
<!-- 标题 --> <!-- 标题 -->
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3"> <div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<h1 class="section-title h4"><i class="bi bi-cpu"></i> 训练配置</h1> <h1 class="section-title h4">
<i class="bi bi-cpu"></i> 训练配置
</h1>
<div class="hint">选择训练/测试数据并设置模型参数</div> <div class="hint">选择训练/测试数据并设置模型参数</div>
</div> </div>
@ -76,28 +106,41 @@
<div class="mb-4"> <div class="mb-4">
<div class="row g-3"> <div class="row g-3">
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<label class="form-label fw-semibold"><i class="bi bi-folder2-open me-1"></i>选择训练文件</label> <label class="form-label fw-semibold">
<i class="bi bi-folder2-open me-1"></i>选择训练文件
</label>
<div class="input-group"> <div class="input-group">
<span class="input-group-text"><i class="bi bi-cloud-arrow-up"></i></span> <span class="input-group-text"><i class="bi bi-cloud-arrow-up"></i></span>
<input id="trainFileInput" type="file" class="form-control" multiple /> <input id="trainFileInput" type="file" class="form-control" multiple />
</div> </div>
<div class="form-text">可多选;支持拖拽到输入框。</div>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<label class="form-label fw-semibold"><i class="bi bi-folder2 me-1"></i>选择测试文件</label> <label class="form-label fw-semibold">
<i class="bi bi-folder2 me-1"></i>选择测试文件
</label>
<div class="input-group"> <div class="input-group">
<span class="input-group-text"><i class="bi bi-cloud-arrow-up"></i></span> <span class="input-group-text"><i class="bi bi-cloud-arrow-up"></i></span>
<input id="testFileInput" type="file" class="form-control" multiple /> <input id="testFileInput" type="file" class="form-control" multiple />
</div> </div>
<div class="form-text">可选;用于评估训练效果。</div>
</div> </div>
</div> </div>
</div> </div>
<hr class="my-4"> <hr class="my-4">
<!-- 参数区(仅美化,不改 id --> <!-- 参数区 -->
<div class="row g-3"> <div class="row g-3">
<div class="col-12 col-lg-8"> <div class="col-12 col-lg-8">
<div class="row g-3"> <!-- 基础设置 -->
<div class="subcard mb-3">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-sliders2-vertical"></i><strong>基础设置</strong>
</div>
<div class="row g-3 g-tight">
<div class="col-12"> <div class="col-12">
<div class="input-group"> <div class="input-group">
<span class="input-group-text"><i class="bi bi-type me-1"></i>model name</span> <span class="input-group-text"><i class="bi bi-type me-1"></i>model name</span>
@ -110,6 +153,7 @@
<span class="input-group-text"><i class="bi bi-image me-1"></i>image filter</span> <span class="input-group-text"><i class="bi bi-image me-1"></i>image filter</span>
<input id="image_filter" type="text" class="form-control" placeholder="_img" /> <input id="image_filter" type="text" class="form-control" placeholder="_img" />
</div> </div>
<div class="form-text"><code>*_img.png</code></div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@ -117,6 +161,7 @@
<span class="input-group-text"><i class="bi bi-brush me-1"></i>masks filter</span> <span class="input-group-text"><i class="bi bi-brush me-1"></i>masks filter</span>
<input id="masks_filter" type="text" class="form-control" placeholder="_masks" /> <input id="masks_filter" type="text" class="form-control" placeholder="_masks" />
</div> </div>
<div class="form-text"><code>*_masks.png</code></div>
</div> </div>
<div class="col-12"> <div class="col-12">
@ -130,7 +175,81 @@
</div> </div>
</div> </div>
<!-- 右侧提示(可删,不影响逻辑) --> <!-- 训练超参 -->
<div class="subcard">
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-rocket-takeoff"></i><strong>训练超参数</strong>
</div>
<div class="row g-3 g-tight">
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-speedometer2 me-1"></i>learning rate</span>
<input id="learning_rate" type="text" class="form-control" placeholder="0.005" />
</div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-hash me-1"></i>n epochs</span>
<input id="n_epochs" type="text" class="form-control" placeholder="100" />
</div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-diagram-2 me-1"></i>weight decay</span>
<input id="weight_decay" type="text" class="form-control" placeholder="0.1" />
</div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-arrows-move me-1"></i>normalize</span>
<select id="normalize" class="form-select">
<option selected value="1">True</option>
<option value="0">False</option>
</select>
</div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-gear me-1"></i>compute flows</span>
<!-- 注意:此处 id 仍为 normalize你原文如此。建议后续改名避免重复 -->
<select id="compute_flows" class="form-select">
<option selected value="0">False</option>
<option value="1">True</option>
</select>
</div>
<div class="form-text">与光流/流场相关,通常仅在特定任务启用。</div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-123 me-1"></i>min train masks</span>
<input id="min_train_masks" type="text" class="form-control" placeholder="5" />
</div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-collection me-1"></i>nimg_per_epoch</span>
<input id="nimg_per_epoch" type="text" class="form-control" placeholder="" />
</div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-layers me-1"></i>channel axis</span>
<input id="channel_axis" type="text" class="form-control" placeholder="" />
</div>
</div>
</div>
</div>
</div>
<!-- 右侧提示 -->
<div class="col-12 col-lg-4"> <div class="col-12 col-lg-4">
<div class="p-3 rounded-3 border h-100"> <div class="p-3 rounded-3 border h-100">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
@ -139,7 +258,8 @@
<div class="hint"> <div class="hint">
• 名称用于区分训练产物(日志/权重)。<br> • 名称用于区分训练产物(日志/权重)。<br>
• filter 用于匹配文件名,如 <code>*_img.png</code><code>*_masks.png</code><br> • filter 用于匹配文件名,如 <code>*_img.png</code><code>*_masks.png</code><br>
• 选择合适的预训练模型可加速收敛。 • 选择合适的预训练模型可加速收敛。<br>
• 建议仅微调必要超参,保持可复现性。
</div> </div>
</div> </div>
</div> </div>
@ -153,12 +273,11 @@
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="d-flex align-items-center justify-content-between mb-1"> <div class="d-flex align-items-center justify-content-between mb-1">
<span class="hint"><i class="bi bi-activity me-1"></i>进度</span> <span class="hint"><i class="bi bi-activity me-1"></i>进度</span>
<span class="hint" id="barText"></span><!-- 如需 JS 写入文字可用;不用也不影响 --> <span class="hint" id="barText"></span>
</div> </div>
<progress id="bar" max="100" value="0"></progress> <progress id="bar" max="100" value="0"></progress>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -239,13 +358,29 @@
const model_name = (document.getElementById('model_name')?.value || '').trim(); const model_name = (document.getElementById('model_name')?.value || '').trim();
const image_filter = (document.getElementById('image_filter')?.value || '').trim(); const image_filter = (document.getElementById('image_filter')?.value || '').trim();
const masks_filter = (document.getElementById('masks_filter')?.value || '').trim(); const masks_filter = (document.getElementById('masks_filter')?.value || '').trim();
const learning_rate = (document.getElementById('learning_rate')?.value || '').trim();
const n_epochs = (document.getElementById('n_epochs')?.value || '').trim();
const weight_decay = (document.getElementById('weight_decay')?.value || '').trim();
const normalize = document.getElementById('normalize')?.value;
const compute_flows = document.getElementById('compute_flows')?.value;
const min_train_masks = (document.getElementById('min_train_masks')?.value || '').trim();
const nimg_per_epoch = (document.getElementById('nimg_per_epoch')?.value || '').trim();
const channel_axis = (document.getElementById('weight_decay')?.value || '').trim();
// 用 URLSearchParams 组装查询串 // 用 URLSearchParams 组装查询串
const qs = new URLSearchParams({ const qs = new URLSearchParams({
model: model, model: model,
model_name: model_name, model_name: model_name,
image_filter: image_filter, image_filter: image_filter,
masks_filter: masks_filter masks_filter: masks_filter,
learning_rate: learning_rate,
n_epochs: n_epochs,
weight_decay: weight_decay,
normalize: normalize,
compute_flows: compute_flows,
min_train_masks: min_train_masks,
nimg_per_epoch: nimg_per_epoch,
channel_axis: channel_axis
}); });
return `${API_UPLOAD}?${qs.toString()}`; return `${API_UPLOAD}?${qs.toString()}`;

View file

@ -1,13 +1,123 @@
<!DOCTYPE html> <!doctype html>
<meta charset="UTF-8" /> <html lang="zh-CN" data-bs-theme="auto">
<h1>训练结果预览</h1>
<p id="none-exist" hidden></p>
<canvas id="lossChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <head>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <meta charset="UTF-8" />
<script src="api.js"></script> <title>训练结果预览</title>
<script type="module">
<!-- Bootstrap & Icons与前页一致 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
/* 背景与玻璃卡片 —— 与你其它页保持一致 */
body {
min-height: 100vh;
background:
radial-gradient(1200px 600px at 20% 10%, rgba(13, 110, 253, .15), transparent 60%),
radial-gradient(800px 400px at 80% 90%, rgba(32, 201, 151, .18), transparent 60%),
linear-gradient(180deg, #f8f9fa, #eef2f7);
}
@media (prefers-color-scheme: dark) {
body {
background:
radial-gradient(1200px 600px at 20% 10%, rgba(13, 110, 253, .25), transparent 60%),
radial-gradient(800px 400px at 80% 90%, rgba(32, 201, 151, .25), transparent 60%),
linear-gradient(180deg, #0b1020, #0e1326);
}
}
.glass-card {
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: rgba(255, 255, 255, .65);
border: 1px solid rgba(255, 255, 255, .5);
}
[data-bs-theme="dark"] .glass-card {
background: rgba(17, 20, 34, .6);
border: 1px solid rgba(255, 255, 255, .08);
}
/* 标题行 */
.section-title {
display: flex;
align-items: center;
gap: .6rem;
margin: 0;
}
.hint {
font-size: .9rem;
opacity: .75
}
/* 画布容器:响应式 16:9可根据需要改比例 */
.chart-wrap {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
/* 最小高度,防止数据少时过扁 */
min-height: 280px;
}
/* 让 canvas 完整填充容器(不改 id仅加样式 */
#lossChart {
position: absolute;
inset: 0;
width: 100% !important;
height: 100% !important;
display: block;
}
/* “无结果”提示:沿用 id不改逻辑仅美化 */
#none-exist {
margin: 0 0 1rem 0;
}
</style>
</head>
<body>
<div class="container py-4">
<div class="glass-card rounded-4 shadow-lg p-4 p-md-5">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<h1 class="section-title h4">
<i class="bi bi-graph-up-arrow"></i> 训练结果预览
</h1>
<div class="hint">Loss / Metric 曲线将根据你的训练日志自动绘制</div>
</div>
<!-- “无结果”占位:保持 id不改 hidden 的使用方式 -->
<p id="none-exist" class="alert alert-warning d-flex align-items-center" role="status" hidden>
<i class="bi bi-exclamation-triangle me-2"></i>
暂无可预览的数据,请先开始训练或检查日志输出。
</p>
<!-- 图表区域 -->
<div class="chart-wrap rounded-3 border">
<canvas id="lossChart"></canvas>
</div>
<!-- 可选:底部小提示(不影响现有 JS -->
<div class="mt-3 text-secondary small">
提示:如果你使用 Chart.js建议设置 <code>responsive: true</code><code>maintainAspectRatio: false</code>
本页样式已自动保证画布自适应容器尺寸。
</div>
</div>
</div>
<!-- Bootstrap Bundle含 Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="api.js"></script>
<script type="module">
const API_RESULT = API_BASE + "status"; const API_RESULT = API_BASE + "status";
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
@ -93,4 +203,7 @@
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
} }
</script> </script>
</body>
</html>