Compare commits

..

No commits in common. "master" and "0.9.1" have entirely different histories.

13 changed files with 182 additions and 1135 deletions

View file

@ -6,13 +6,8 @@
<img alt="Static Badge" src="https://img.shields.io/badge/Redis-6.4.0-red"> <img alt="Static Badge" src="https://img.shields.io/badge/Redis-6.4.0-red">
<img alt="Static Badge" src="https://img.shields.io/badge/JSDelivr-in_use-brown"> <img alt="Static Badge" src="https://img.shields.io/badge/JSDelivr-in_use-brown">
<img alt="Static Badge" src="https://img.shields.io/badge/Flask-3.1.2-8ecae6"> <img alt="Static Badge" src="https://img.shields.io/badge/Flask-3.1.2-8ecae6">
<br><br>
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/da5b891e-b4ac-484a-885c-0856f18e04fc" style="height: 70%; width: 70%"/>
</p> </p>
<br> <br>
🌈 实现功能: 🌈 实现功能:
@ -21,18 +16,16 @@
- ⛵️ 训练、分割结果随时下载 - ⛵️ 训练、分割结果随时下载
- 📚 权重可直接作为后续分割模型 - 📚 权重可直接作为后续分割模型
- 🛠️ 一键安装部署脚本 - 🛠️ 一键安装部署脚本
- 🎨 前端样式美化 - 🚧 [TODO] 前端样式美化
<br> <br>
## 🚀一键安装 ## 🚀一键安装
<b>[最新Release页面](https://github.com/ClovertaTheTrilobita/cellpose-web/releases/latest)</b>中下载最新的 <b>install.sh</b> 到你的Linux/macOS机器上。 <b>[Release页面](https://github.com/ClovertaTheTrilobita/cellpose-web/releases)</b>中下载最新的 <b>install.sh</b> 到你的Linux/macOS机器上。
将它放到你希望项目存在的位置,并执行它,安装脚本会将项目自动拉取到同一目录下。 将它放到你希望项目存在的位置,并执行它,安装脚本会将项目自动拉取到同一目录下。
**NOTE:** 安装脚本设计上支持Debian系、Arch系、RHEL系Linux,但目前仅测试过Arch Linux。其他发行版暂未经过测试若出现意外错误推荐手动安装。
Windows暂时不支持通过脚本一键安装。 Windows暂时不支持通过脚本一键安装。
<br> <br>

View file

@ -1,5 +1,5 @@
backend: backend:
ip: 192.168.193.141 ip: 10.10.25.240
port: 5000 port: 5000
model: model:

View file

@ -62,20 +62,6 @@ class Cprun:
diameter: float | None = None, diameter: float | None = None,
flow_threshold: float = 0.4, flow_threshold: float = 0.4,
cellprob_threshold: float = 0.0, ): cellprob_threshold: float = 0.0, ):
"""
运行 cellpose 分割
Args:
images: [list] 图片存储路径
time: [str] 开始运行的时间相当于本次运行的ID用于存储运行结果
model: [str] 图像分割所使用的模型
diameter: [float] diameters are used to rescale the image to 30 pix cell diameter.
flow_threshold: [float] flow error threshold (all cells with errors below threshold are kept) (not used for 3D). Defaults to 0.4.
cellprob_threshold: [float] all pixels with value above threshold kept for masks, decrease to find more and larger masks. Defaults to 0.0.
Returns:
"""
if time is None: if time is None:
return [False, "No time received"] return [False, "No time received"]
@ -85,10 +71,9 @@ class Cprun:
message = [f"Using {model} model"] message = [f"Using {model} model"]
# 设定模型参数
model = models.CellposeModel(gpu=True, model_type=model) model = models.CellposeModel(gpu=True, model_type=model)
files = images files = images
imgs = [imread(f) for f in files] # 获取目录中的每一个文件 imgs = [imread(f) for f in files]
masks, flows, styles = model.eval( masks, flows, styles = model.eval(
imgs, imgs,
flow_threshold=flow_threshold, flow_threshold=flow_threshold,
@ -105,7 +90,7 @@ class Cprun:
out = base + "_output" out = base + "_output"
save_masks(imgs, mask, flow, out, tif=True) save_masks(imgs, mask, flow, out, tif=True)
# 用 plot 生成彩色叠加图 # 用 plot 生成彩色叠加图(不依赖 skimage
rgb = plot.image_to_rgb(img, channels=[0, 0]) # 原图转 RGB rgb = plot.image_to_rgb(img, channels=[0, 0]) # 原图转 RGB
over = plot.mask_overlay(rgb, masks=mask, colors=None) # 叠加彩色实例 over = plot.mask_overlay(rgb, masks=mask, colors=None) # 叠加彩色实例
Image.fromarray(over).save(base + "_overlay.png") Image.fromarray(over).save(base + "_overlay.png")

View file

@ -5,8 +5,6 @@ import redis
import datetime import datetime
import json import json
from sympy import false
CONFIG_PATH = Path(__file__).parent / "config.yaml" CONFIG_PATH = Path(__file__).parent / "config.yaml"
cfg = OmegaConf.load(CONFIG_PATH) cfg = OmegaConf.load(CONFIG_PATH)
cfg.data.root_dir = str((CONFIG_PATH.parent / cfg.data.root_dir).resolve()) cfg.data.root_dir = str((CONFIG_PATH.parent / cfg.data.root_dir).resolve())
@ -19,19 +17,6 @@ os.environ["CELLPOSE_LOCAL_MODELS_PATH"] = MODELS_DIR
r = redis.Redis(host="127.0.0.1", port=6379, db=0) r = redis.Redis(host="127.0.0.1", port=6379, db=0)
def set_status(task_id, status, train_losses, test_losses, **extra): def set_status(task_id, status, train_losses, test_losses, **extra):
"""
修改redis数据库中某一任务的运行状态
Args:
task_id: 这一任务的时间戳
status: 任务状态
train_losses: 此次任务的训练loss
test_losses: 此次任务的测试loss
**extra:
Returns:
"""
payload = {"status": status, payload = {"status": status,
"updated_at": datetime.datetime.utcnow().isoformat(), "updated_at": datetime.datetime.utcnow().isoformat(),
"train_losses": train_losses.tolist() if hasattr(train_losses, "tolist") else train_losses, "train_losses": train_losses.tolist() if hasattr(train_losses, "tolist") else train_losses,
@ -49,51 +34,11 @@ class Cptrain:
@classmethod @classmethod
async def start_train(cls, async def start_train(cls,
time: str | None = None, time: str | None = None,
model_name: str | None = None, model_name: str | None = None,
image_filter: str = "_img", image_filter: str = "_img",
mask_filter: str = "_masks", mask_filter: str = "_masks",
base_model: str = "cpsam", base_model: str = "cpsam"):
train_probs: list[float] = None,
test_probs: list[float] = None,
batch_size: int = 8,
learning_rate = 5e-5,
n_epochs: int = 100,
weight_decay=0.1,
normalize: bool =True,
compute_flows: bool = False,
min_train_masks: int = 5,
nimg_per_epoch: int =None,
rescale: bool= False,
scale_range=None,
channel_axis: int = None,
):
"""
开始训练
Args:
time: 此次任务的时间戳即任务ID
model_name: 训练结果命名
image_filter:
mask_filter:
base_model:
train_probs:
test_probs:
batch_size:
learning_rate:
n_epochs:
weight_decay:
normalize:
compute_flows:
min_train_masks:
nimg_per_epoch:
rescale:
scale_range:
channel_axis:
Returns:
"""
train_dir = Path(TRAIN_DIR) / time train_dir = Path(TRAIN_DIR) / time
test_dir = Path(TEST_DIR) / time test_dir = Path(TEST_DIR) / time
@ -111,13 +56,9 @@ class Cptrain:
model_path, train_losses, test_losses = train.train_seg(model.net, model_path, train_losses, test_losses = train.train_seg(model.net,
train_data=images, train_labels=labels, train_data=images, train_labels=labels,
test_data=test_images, test_labels=test_labels, test_data=test_images, test_labels=test_labels,
train_probs=train_probs, test_probs=test_probs, weight_decay=0.1, learning_rate=1e-5,
weight_decay=weight_decay, learning_rate=learning_rate, n_epochs=100, model_name=model_name,
n_epochs=n_epochs, model_name=model_name, save_path=BASE_DIR)
save_path=BASE_DIR, batch_size=batch_size,
normalize=normalize, compute_flows=compute_flows, min_train_masks=min_train_masks,
nimg_per_epoch=nimg_per_epoch, rescale=rescale, scale_range=scale_range, channel_axis=channel_axis
)
set_status(time, "done", train_losses, test_losses) set_status(time, "done", train_losses, test_losses)
print("模型已保存到:", model_path) print("模型已保存到:", model_path)

View file

@ -93,10 +93,15 @@ def run_upload():
return default return default
flow_threshold = _to_float(request.args.get("flow_threshold") or request.form.get("flow_threshold"), 0.4) flow_threshold = _to_float(request.args.get("flow_threshold") or request.form.get("flow_threshold"), 0.4)
cellprob_threshold = _to_float(request.args.get("cellprob_threshold") or request.form.get("cellprob_threshold"),0.0) cellprob_threshold = _to_float(request.args.get("cellprob_threshold") or request.form.get("cellprob_threshold"),
0.0)
diameter_raw = request.args.get("diameter") or request.form.get("diameter") diameter_raw = request.args.get("diameter") or request.form.get("diameter")
diameter = _to_float(diameter_raw, None) if diameter_raw not in (None, "") else None diameter = _to_float(diameter_raw, None) if diameter_raw not in (None, "") else None
print("cpt:" + str(cellprob_threshold))
print("flow:" + str(flow_threshold))
print("diameter:" + str(diameter))
# 将文件保存在本地目录中 # 将文件保存在本地目录中
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}"
os.makedirs(Path(UPLOAD_DIR) / ts, exist_ok=True) os.makedirs(Path(UPLOAD_DIR) / ts, exist_ok=True)
@ -135,69 +140,12 @@ def run_upload():
@app.post("/train_upload") @app.post("/train_upload")
def train_upload(): def train_upload():
"""
从前端获取训练数据和测试数据并开始训练
Returns:
"""
def _to_float(x, default):
"""
将变量转为float类型
Args:
x: 变量
default: 默认值
"""
try:
return float(x)
except (TypeError, ValueError):
return default
def _to_int(x, default):
"""
将变量转为int类型
Args:
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 = _to_int(request.args.get("batch_size"), 8)
learning_rate = _to_float(request.args.get("learning_rate"), 5e-5)
n_epochs = _to_int(request.args.get("n_epochs"), 100)
weight_decay = _to_float(request.args.get("weight_decay"), 0.1)
normalize = request.args.get(
"normalize",
default=True,
type=lambda v: str(v).strip().lower() in ("1","true","t","yes","y","on")
)
compute_flows = request.args.get(
"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")
os.makedirs(Path(TRAIN_DIR) / ts, exist_ok=True) os.makedirs(Path(TRAIN_DIR) / ts, exist_ok=True)
@ -219,30 +167,23 @@ def train_upload():
saved.append(os.path.join(TEST_DIR, ts, name)) saved.append(os.path.join(TEST_DIR, ts, name))
def job(): def job():
"""
子线程方法
"""
return asyncio.run(Cptrain.start_train( return asyncio.run(Cptrain.start_train(
time=ts, model_name=model_name, image_filter=image_filter, mask_filter=mask_filter, base_model=base_model, time=ts,
batch_size=batch_size, learning_rate=learning_rate, n_epochs=n_epochs, weight_decay=weight_decay, model_name=model_name,
normalize=normalize, compute_flows=compute_flows, min_train_masks=min_train_masks, nimg_per_epoch=nimg_per_epoch, image_filter=image_filter,
rescale=rescale, scale_range=scale_range, channel_axis=channel_axis, mask_filter=mask_filter,
base_model=base_model
)) ))
# 创建一个子线程,防止阻塞主线程
fut = executor.submit(job) fut = executor.submit(job)
def done_cb(f): def done_cb(f):
"""
获取训练结果并存入redis数据库
"""
try: try:
train_losses, test_losses = f.result() train_losses, test_losses = f.result()
set_train_status(ts, "success", train_losses, test_losses) set_train_status(ts, "success", train_losses, test_losses)
except Exception as e: except Exception as e:
set_status(ts, "failed", error=str(e)) set_status(ts, "failed", error=str(e))
# 添加回调在子线程执行完后更新redis中任务状态
fut.add_done_callback(done_cb) fut.add_done_callback(done_cb)
return jsonify({"ok": True, "count": len(saved), "id": ts}) return jsonify({"ok": True, "count": len(saved), "id": ts})
@ -256,20 +197,12 @@ def status():
""" """
task_id = request.args.get('id') task_id = request.args.get('id')
st = get_status(task_id) st = get_status(task_id)
print(st)
if not st: if not st:
return jsonify({"ok": True, "exists": False, "status": "not_found"}), 200 return jsonify({"ok": True, "exists": False, "status": "not_found"}), 200
return jsonify({"ok": True, "exists": True, **st}), 200 return jsonify({"ok": True, "exists": True, **st}), 200
@app.get("/preview") @app.get("/preview")
def preview(): def preview():
"""
获取本次分割结果的预览
Returns:
"""
task_id = request.args.get('id') task_id = request.args.get('id')
task_dir = Path(OUTPUT_DIR) / task_id task_dir = Path(OUTPUT_DIR) / task_id
if not task_dir.exists(): if not task_dir.exists():
@ -294,24 +227,11 @@ def preview():
@app.get("/models") @app.get("/models")
def list_models(): def list_models():
""" models_list = os.listdir(MODELS_DIR)
获取现有模型列表
Returns:
"""
models_list = os.listdir(MODELS_DIR) # 查询模型列表中有哪些文件
return jsonify({"ok": True, "models": models_list}) return jsonify({"ok": True, "models": models_list})
@app.get("/result") @app.get("/result")
def list_results(): def list_results():
"""
获取运行结果
Returns:
"""
task_id = request.args.get('id') task_id = request.args.get('id')
st = get_status(task_id) st = get_status(task_id)
if not st: if not st:

View file

@ -4,7 +4,7 @@ from multiprocessing import Process
if __name__ == "__main__": if __name__ == "__main__":
# 启动测试服务器 # Cprun.run_test()
p = Process(target=run_dev) p = Process(target=run_dev)
p.start() p.start()
print(f"Flask running in PID {p.pid}") print(f"Flask running in PID {p.pid}")

View file

@ -1,7 +1,7 @@
const config = { const config = {
server: { server: {
protocol: 'http', protocol: 'http',
host: '192.168.193.141', host: '10.10.25.240',
port: 5000 port: 5000
} }
}; };

View file

@ -1,139 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN" data-bs-theme="auto"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>任务面板</title> <title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
<!-- Bootstrap CSS --> integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<!-- Bootstrap Icons -->
<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, 0.65);
border: 1px solid rgba(255, 255, 255, 0.5);
}
[data-bs-theme="dark"] .glass-card {
background: rgba(17, 20, 34, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.action-btn {
transition: transform .15s ease, box-shadow .15s ease;
padding: 1.1rem 1.4rem;
font-size: 1.05rem;
border-radius: 1rem;
}
.action-btn:hover, .action-btn:focus-visible {
transform: translateY(-2px);
box-shadow: 0 0.75rem 1.25rem rgba(0,0,0,.12);
}
.btn-icon {
display: inline-flex;
align-items: center;
gap: .6rem;
}
.brand-badge {
letter-spacing: .08em;
background: rgba(13,110,253,.1);
color: #0d6efd;
border: 1px solid rgba(13,110,253,.15);
}
[data-bs-theme="dark"] .brand-badge {
background: rgba(13,110,253,.15);
color: #9ec5fe;
border-color: rgba(13,110,253,.25);
}
</style>
</head> </head>
<body> <body>
<main class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
<div class="col-12 col-md-10 col-lg-8">
<div class="glass-card rounded-4 shadow-lg p-4 p-md-5 text-center">
<div class="d-flex justify-content-center mb-3">
<span class="badge brand-badge rounded-pill px-3 py-2">
<i class="bi bi-rocket-takeoff me-1"></i> Cell Processing with <a href="https://github.com/MouseLand/cellpose">CELLPOSE</a>
</span>
</div>
<h1 class="display-6 fw-semibold mb-2">欢迎使用任务面板</h1> <a href="run.html">运行</a>
<p class="text-secondary mb-4"> <a href="train.html">训练</a>
选择你要进行的操作。可随时返回此页切换任务。
</p>
<div class="row g-3 g-md-4 justify-content-center">
<div class="col-12 col-sm-6">
<a href="run.html" class="btn btn-primary w-100 action-btn btn-icon" role="button" aria-label="进入运行页面">
<i class="bi bi-play-circle-fill fs-4"></i>
<span>
运行<br>
<small class="opacity-75">快速执行已有模型/流程</small>
</span>
</a>
</div>
<div class="col-12 col-sm-6">
<a href="train.html" class="btn btn-outline-primary w-100 action-btn btn-icon" role="button" aria-label="进入训练页面">
<i class="bi bi-cpu-fill fs-4"></i>
<span>
训练<br>
<small class="opacity-75">启动或继续训练任务</small>
</span>
</a>
</div>
</div>
<hr class="my-4 my-md-5">
<div class="d-flex flex-column flex-md-row gap-2 justify-content-center">
<a href="https://github.com/ClovertaTheTrilobita/cellpose-web" class="link-secondary text-decoration-none">
<i class="bi bi-github"></i> Github
</a>
<span class="text-secondary d-none d-md-inline"></span>
<a href="#" class="link-secondary text-decoration-none">
<i class="bi bi-gear"></i> 设置
</a>
<span class="text-secondary d-none d-md-inline"></span>
<span class="text-secondary"><i class="bi bi-keyboard"></i> 快捷键:<kbd>R</kbd> 运行,<kbd>T</kbd> 训练</span>
</div>
</div>
</div>
</main>
<!-- 键盘快捷键(可选) -->
<script>
window.addEventListener('keydown', (e) => {
if (['INPUT','TEXTAREA'].includes((e.target.tagName || '').toUpperCase())) return;
if (e.key.toLowerCase() === 'r') location.href = 'run.html';
if (e.key.toLowerCase() === 't') location.href = 'train.html';
});
</script>
<!-- 只需引入 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>
</body> </body>
</html>
</html>

View file

@ -1,149 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN" data-bs-theme="auto"> <meta charset="UTF-8" />
<head> <h1>运行结果预览</h1>
<meta charset="UTF-8" /> <p id="none-exist" hidden></p>
<title>运行结果预览</title> <div id="gallery"></div>
<button onclick="downloadTif()">下载tif</button>
<!-- 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, 0.65);
border: 1px solid rgba(255, 255, 255, 0.5);
}
[data-bs-theme="dark"] .glass-card {
background: rgba(17, 20, 34, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
/* 标题区 */
.page-title {
display: flex;
align-items: center;
gap: .6rem;
margin-bottom: 0;
}
.sub-hint {
font-size: .9rem;
opacity: .75;
}
/* 预览网格:仅美化 #gallery保持 id 不变 */
#gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
/* 预览卡片 */
.thumb {
position: relative;
overflow: hidden;
border-radius: 1rem;
background: rgba(0,0,0,.05);
border: 1px solid rgba(0,0,0,.06);
transition: transform .12s ease, box-shadow .12s ease;
}
.thumb:hover {
transform: translateY(-2px);
box-shadow: 0 .9rem 1.2rem rgba(0,0,0,.12);
}
.thumb img, .thumb canvas, .thumb video {
width: 100%;
height: 180px;
object-fit: cover;
display: block;
}
.thumb .badge-overlay {
position: absolute;
top: .5rem;
left: .5rem;
padding: .25rem .5rem;
border-radius: .5rem;
background: rgba(13,110,253,.85);
color: #fff;
font-size: .75rem;
}
/* 空态提示:仅美化 #none-exist */
#none-exist {
margin: 0;
padding: 1rem 1.25rem;
border-radius: .75rem;
background: rgba(108,117,125,.08);
border: 1px dashed rgba(108,117,125,.35);
color: inherit;
}
/* 下载按钮 */
.download-wrap {
gap: .75rem;
}
.download-btn {
padding: .65rem 1.1rem;
border-radius: .8rem;
transition: transform .12s ease, box-shadow .12s ease;
}
.download-btn:hover, .download-btn:focus-visible {
transform: translateY(-1px);
box-shadow: 0 .75rem 1.25rem rgba(0,0,0,.12);
}
</style>
</head>
<body>
<div class="container py-4">
<div class="glass-card rounded-4 shadow-lg p-4 p-md-5">
<!-- 标题区(保持语义,未改 id -->
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
<h1 class="page-title h4">
<i class="bi bi-images"></i> 运行结果预览
</h1>
<div class="sub-hint">
生成的图像/掩膜将显示在下方网格中
</div>
</div>
<!-- 空态提示:仅样式增强,不改 id/hidden 机制 -->
<p id="none-exist" hidden></p>
<!-- 预览网格:只美化 #gallery -->
<div id="gallery"></div>
<!-- 下载按钮:保留 onclick不动逻辑只加样式 -->
<div class="mt-4 d-flex download-wrap">
<button class="btn btn-primary d-inline-flex align-items-center download-btn"
onclick="downloadTif()">
<i class="bi bi-download me-2"></i> 下载 TIF
</button>
</div>
</div>
</div>
<!-- 仅需引入 bundle如你已有可省略 -->
<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/axios/dist/axios.min.js"></script>
<script src="api.js"></script> <script src="api.js"></script>
<script type="module"> <script type="module">
@ -205,6 +66,4 @@
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
} }
</script> </script>
</body>
</html>

View file

@ -4,14 +4,13 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>运行分割</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>
<!-- <div style="padding: 1rem 1rem;"> <div style="padding: 1rem 1rem;">
<div class="mb-3 border" style="padding: 1rem 1rem;"> <div class="mb-3 border" style="padding: 1rem 1rem;">
<div class="input-group mb-3"> <div class="input-group mb-3">
<input id="fileInput" type="file" class="form-control" multiple /> <input id="fileInput" type="file" class="form-control" multiple />
@ -48,94 +47,7 @@
</div> </div>
</div> </div>
</div> </div>
<br><br><br> --> <br><br><br>
<div class="container py-4">
<div class="glass-card rounded-4 shadow-lg p-4 p-md-5">
<!-- 上传区 -->
<div class="mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<h2 class="h5 section-title mb-0"><i class="bi bi-folder2-open me-2"></i>选择文件</h2>
<span class="hint">可多选</span>
</div>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-cloud-arrow-up"></i></span>
<input id="fileInput" type="file" class="form-control" multiple />
</div>
</div>
<hr class="my-4">
<!-- 参数区 -->
<div class="row g-3">
<div class="col-12 col-lg-8">
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-speedometer2 me-1"></i>flow threshold</span>
<input id="flow" 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-bezier2 me-1"></i>cellprob threshold</span>
<input id="cellprob" 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-bounding-box-circles me-1"></i>diameter</span>
<input id="diameter" type="text" class="form-control" placeholder="" />
</div>
</div>
<div class="col-12 col-md-6">
<label class="w-100">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-cpu-fill me-1"></i>model</span>
<select id="model" class="form-select"></select>
</div>
</label>
</div>
</div>
</div>
<!-- 右侧说明(可删) -->
<div class="col-12 col-lg-4">
<div class="p-3 rounded-3 border h-100">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-info-circle me-2"></i>
<strong>提示</strong>
</div>
<div class="hint">
• 参数留空将使用默认值。<br>
• 数值可使用小数(如 0.4)。<br>
• 选择合适的模型以获得更稳健的分割效果。
</div>
</div>
</div>
</div>
<!-- 行动区 -->
<div class="mt-4 d-flex flex-column flex-md-row align-items-stretch gap-3">
<button id="uploadBtn" class="btn btn-success d-inline-flex align-items-center">
<i class="bi bi-upload me-2"></i> Upload
</button>
<div class="flex-grow-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" id="barText"></span><!-- 如需,你的 JS 可写入文本 -->
</div>
<progress id="bar" max="100" value="0"></progress>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
@ -272,82 +184,6 @@
} }
}); });
</script> </script>
<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, 0.65);
border: 1px solid rgba(255, 255, 255, 0.5);
}
[data-bs-theme="dark"] .glass-card {
background: rgba(17, 20, 34, 0.6);
border: 1px solid rgba(255, 255, 255, 0.08);
}
/* 区块与控件 */
.section-title {
font-weight: 600;
letter-spacing: .02em;
}
.input-group-text i {
opacity: .7;
}
.hint {
font-size: .875rem;
opacity: .75;
}
/* 原生 <progress> 美化(保持 id 不变) */
#bar {
width: 100%;
height: 1rem;
border: none;
border-radius: .75rem;
background-color: rgba(13,110,253,.15);
overflow: hidden;
vertical-align: middle;
}
/* Chrome / Edge / Safari */
#bar::-webkit-progress-bar {
background-color: transparent;
}
#bar::-webkit-progress-value {
background: linear-gradient(90deg, #0d6efd, #6ea8fe);
border-radius: .75rem;
}
/* Firefox */
#bar::-moz-progress-bar {
background: linear-gradient(90deg, #0d6efd, #6ea8fe);
border-radius: .75rem;
}
/* 上传按钮动效 */
#uploadBtn {
padding: .65rem 1.1rem;
border-radius: .8rem;
transition: transform .12s ease, box-shadow .12s ease;
}
#uploadBtn:hover, #uploadBtn:focus-visible {
transform: translateY(-1px);
box-shadow: 0 .75rem 1.25rem rgba(0,0,0,.12);
}
</style>
</body> </body>
</html> </html>

View file

@ -1,290 +1,58 @@
<!doctype html> <!doctype html>
<html lang="zh-CN" data-bs-theme="auto"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>训练任务</title> <title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
<!-- Bootstrap 5.3 与 Icons与前页一致 --> integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<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-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);
}
/* input-group 统一图标透明度 */
.input-group-text i { opacity: .8; }
/* progress 美化(保持 id 不变) */
#bar {
width: 100%;
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;
}
/* 上传/启动按钮动效(保持 id 不变) */
#uploadBtn {
padding: .65rem 1.1rem;
border-radius: .8rem;
transition: transform .12s ease, box-shadow .12s ease;
}
#uploadBtn:hover, #uploadBtn:focus-visible {
transform: translateY(-1px);
box-shadow: 0 .75rem 1.25rem rgba(0,0,0,.12);
}
/* 更紧凑的行距 */
.g-tight { row-gap: .75rem; }
</style>
</head> </head>
<body> <body>
<div class="container py-4"> <div style="padding: 1rem 1rem;">
<div class="glass-card rounded-4 shadow-lg p-4 p-md-5"> <div class="mb-3 border" style="padding: 1rem 1rem;">
<!-- 标题 --> <div class="input-group mb-3">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3"> <h5>选择训练文件</h5>
<h1 class="section-title h4"> <input id="trainFileInput" type="file" class="form-control" multiple />
<i class="bi bi-cpu"></i> 训练配置
</h1>
<div class="hint">选择训练/测试数据并设置模型参数</div>
</div>
<!-- 选择文件 -->
<div class="mb-4">
<div class="row g-3">
<div class="col-12 col-lg-6">
<label class="form-label fw-semibold">
<i class="bi bi-folder2-open me-1"></i>选择训练文件
</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-cloud-arrow-up"></i></span>
<input id="trainFileInput" type="file" class="form-control" multiple />
</div> </div>
<div class="form-text">可多选;支持拖拽到输入框。</div> <div class="input-group mb-3">
</div> <h5>选择测试文件</h5>
<input id="testFileInput" type="file" class="form-control" multiple />
<div class="col-12 col-lg-6"> </div>
<label class="form-label fw-semibold">
<i class="bi bi-folder2 me-1"></i>选择测试文件 <hr>
</label> <div>
<div class="input-group"> <div style="padding-right: 50rem;">
<span class="input-group-text"><i class="bi bi-cloud-arrow-up"></i></span> <div class="input-group mb-3">
<input id="testFileInput" type="file" class="form-control" multiple /> <span class="input-group-text">model name:</span>
<input id="name" type="text" class="form-control" placeholder="为你的模型命名" />
</div>
<div class="input-group mb-3">
<span class="input-group-text">image filter:</span>
<input id="image_filter" type="text" class="form-control" placeholder="_img" />
</div>
<div class="input-group mb-3">
<span class="input-group-text">masks filter</span>
<input id="masks_filter" type="text" class="form-control" placeholder="_masks" />
</div>
<label>
<select id="model" class="form-select"></select>
</label>
</label>
</div>
</div>
<br>
<div>
<button id="uploadBtn" class="btn btn-success">Upload</button>
<progress id="bar" max="100" value="0" style="width:300px;"></progress>
</div> </div>
<div class="form-text">可选;用于评估训练效果。</div>
</div>
</div> </div>
</div>
<hr class="my-4">
<!-- 参数区 -->
<div class="row g-3">
<div class="col-12 col-lg-8">
<!-- 基础设置 -->
<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="input-group">
<span class="input-group-text"><i class="bi bi-type me-1"></i>model name</span>
<input id="name" 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-image me-1"></i>image filter</span>
<input id="image_filter" type="text" class="form-control" placeholder="_img" />
</div>
<div class="form-text"><code>*_img.png</code></div>
</div>
<div class="col-12 col-md-6">
<div class="input-group">
<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" />
</div>
<div class="form-text"><code>*_masks.png</code></div>
</div>
<div class="col-12">
<label class="w-100">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-cpu-fill me-1"></i>model</span>
<select id="model" class="form-select"></select>
</div>
</label>
</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="p-3 rounded-3 border h-100">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-info-circle me-2"></i><strong>提示</strong>
</div>
<div class="hint">
• 名称用于区分训练产物(日志/权重)。<br>
• filter 用于匹配文件名,如 <code>*_img.png</code><code>*_masks.png</code><br>
• 选择合适的预训练模型可加速收敛。<br>
• 建议仅微调必要超参,保持可复现性。
</div>
</div>
</div>
</div>
<!-- 行动与进度(保持 id 与机制不变) -->
<div class="mt-4 d-flex flex-column flex-md-row align-items-stretch gap-3">
<button id="uploadBtn" class="btn btn-success d-inline-flex align-items-center">
<i class="bi bi-play-circle me-2"></i> Upload
</button>
<div class="flex-grow-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" id="barText"></span>
</div>
<progress id="bar" max="100" value="0"></progress>
</div>
</div>
</div> </div>
</div> <br><br><br>
<!-- 只需引入 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/axios/dist/axios.min.js"></script>
@ -358,29 +126,13 @@
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,214 +1,96 @@
<!doctype html> <!DOCTYPE html>
<html lang="zh-CN" data-bs-theme="auto"> <meta charset="UTF-8" />
<h1>训练结果预览</h1>
<p id="none-exist" hidden></p>
<canvas id="lossChart"></canvas>
<head> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<meta charset="UTF-8" /> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<title>训练结果预览</title> <script src="api.js"></script>
<script type="module">
<!-- Bootstrap & Icons与前页一致 --> const API_RESULT = API_BASE + "status";
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" const params = new URLSearchParams(window.location.search);
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> const ID = params.get("id");
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style> const msg = document.getElementById("none-exist");
/* 背景与玻璃卡片 —— 与你其它页保持一致 */ const cava = document.getElementById("lossChart")
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) { if (!ID) {
body { msg.textContent = "missing id in URL";
background: msg.hidden = false;
radial-gradient(1200px 600px at 20% 10%, rgba(13, 110, 253, .25), transparent 60%), cava.hidden = true;
radial-gradient(800px 400px at 80% 90%, rgba(32, 201, 151, .25), transparent 60%), } else {
linear-gradient(180deg, #0b1020, #0e1326); try {
} const res = await axios.get(API_RESULT + "?id=" + encodeURIComponent(ID));
} console.log(res);
const { exists, status } = res.data; // exists: boolean, status: "running" | "success" | "failed"...
const data = res.data;
.glass-card { if (!exists) {
backdrop-filter: blur(10px); msg.textContent = `id "${ID}" 不存在`;
-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>
<!-- 底部小提示 -->
<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 params = new URLSearchParams(window.location.search);
const ID = params.get("id");
const msg = document.getElementById("none-exist");
const cava = document.getElementById("lossChart")
if (!ID) {
msg.textContent = "missing id in URL";
msg.hidden = false;
cava.hidden = true;
} else {
try {
const res = await axios.get(API_RESULT + "?id=" + encodeURIComponent(ID));
console.log(res);
const { exists, status } = res.data; // exists: boolean, status: "running" | "success" | "failed"...
const data = res.data;
if (!exists) {
msg.textContent = `任务 "${ID}" 不存在`;
msg.hidden = false;
cava.hidden = true;
}
else if (status == "running") {
msg.textContent = `任务 "${ID}" 仍在运行中,请耐心等待片刻后刷新。`;
msg.hidden = false;
cava.hidden = true;
}
else if (status == "failed") {
msg.textContent = `任务 "${ID}" 运行失败,由于:"Error: ${data.error}",请检查上传数据集是否存在问题`;
msg.hidden = false;
cava.hidden = true;
}
else {
msg.hidden = true;
cava.hidden = false;
const train_losses = data.train_losses;
const test_losses = data.test_losses;
const epochs = Array.from({ length: Math.max(train_losses.length, test_losses.length) }, (_, i) => i + 1);
const ctx = document.getElementById('lossChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: epochs,
datasets: [
{
label: 'Train Loss',
data: train_losses,
borderColor: 'blue',
fill: false,
tension: 0.2,
},
{
label: 'Test Loss',
data: test_losses,
borderColor: 'red',
fill: false,
tension: 0.2,
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
scales: {
x: { title: { display: true, text: 'Epoch' } },
y: { title: { display: true, text: 'Loss' } }
}
}
});
}
} catch (e) {
msg.textContent = "请求失败";
msg.hidden = false; msg.hidden = false;
console.error(e); cava.hidden = true;
} }
} else if (status == "running") {
msg.textContent = `id "${ID}" 仍在运行中,请耐心等待片刻后刷新。`;
msg.hidden = false;
cava.hidden = true;
}
else {
msg.hidden = true;
cava.hidden = false;
window.downloadTif = function () { const train_losses = data.train_losses;
const a = document.createElement("a"); const test_losses = data.test_losses;
a.href = API_DL + "?id=" + encodeURIComponent(ID);
a.download = ID; // 留空:文件名由服务端决定;可写具体名称
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
</script>
</body>
</html> const epochs = Array.from({ length: Math.max(train_losses.length, test_losses.length) }, (_, i) => i + 1);
const ctx = document.getElementById('lossChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: epochs,
datasets: [
{
label: 'Train Loss',
data: train_losses,
borderColor: 'blue',
fill: false,
tension: 0.2,
},
{
label: 'Test Loss',
data: test_losses,
borderColor: 'red',
fill: false,
tension: 0.2,
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
scales: {
x: { title: { display: true, text: 'Epoch' } },
y: { title: { display: true, text: 'Loss' } }
}
}
});
}
} catch (e) {
msg.textContent = "请求失败";
msg.hidden = false;
console.error(e);
}
}
window.downloadTif = function () {
const a = document.createElement("a");
a.href = API_DL + "?id=" + encodeURIComponent(ID);
a.download = ID; // 留空:文件名由服务端决定;可写具体名称
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
</script>

View file

@ -434,6 +434,6 @@ else
fi fi
echo -e "${GREEN}==>${RESET} Deployment successfull" echo -e "${GREEN}==>${RESET} Deployment successfull"
if ask_yn "Do you wish to start cellpose developmental server now?" y; then if ask_yn "Do you wish to start cellpose server now?" y; then
python ${root}/backend/main.py python ${root}/backend/main.py
fi fi