138 lines
5.7 KiB
HTML
138 lines
5.7 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>72 号接口 · B 端五个关键点</title>
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; background: #eef2f5; color: #192d3d; font: 16px/1.55 system-ui, sans-serif; }
|
||
main { width: min(1040px, 100%); margin: auto; padding: 26px 18px; }
|
||
h1 { margin: 0; font-size: clamp(24px, 4vw, 35px); }
|
||
h2 { margin: 0 0 10px; font-size: 19px; }
|
||
p { margin: 8px 0; }
|
||
.note { color: #4f6577; }
|
||
.controls, .panel { background: white; border-radius: 12px; padding: 18px; margin-top: 16px; }
|
||
form { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
|
||
button { background: #156a72; color: white; border: 0; border-radius: 7px; padding: 9px 15px; font: inherit; cursor: pointer; }
|
||
button:disabled { opacity: .55; cursor: wait; }
|
||
input { max-width: 100%; }
|
||
#status[data-error="true"] { color: #a22629; }
|
||
.results { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 16px; }
|
||
[hidden] { display: none; }
|
||
.photo { display: grid; border-radius: 8px; overflow: hidden; background: #1b2c37; }
|
||
.photo img { grid-area: 1 / 1; width: 100%; display: block; }
|
||
.panel a { color: #156a72; }
|
||
table { width: 100%; border-collapse: collapse; margin: 8px 0 20px; font-variant-numeric: tabular-nums; }
|
||
th, td { text-align: left; border-bottom: 1px solid #e6edf2; padding: 5px 7px; }
|
||
@media(max-width: 680px) { .results { grid-template-columns: 1fr; gap: 0; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>72 号接口 · B 端五个关键点</h1>
|
||
<p class="note">上传正面照,查看扶正裁切后的六个白点、四条虚线和厘米标注。</p>
|
||
<form id="client-auth" method="post" action="/api/v1/auth/login" style="display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:12px 0">
|
||
<label>用户名 <input name="username" autocomplete="username" required></label>
|
||
<label>密码 <input name="password" type="password" autocomplete="current-password" required></label>
|
||
<button type="submit">登录</button>
|
||
<button id="client-auth-check" type="button">测试鉴权</button>
|
||
<button id="client-auth-refresh" type="button" disabled>刷新令牌</button>
|
||
<span id="client-auth-status" role="status">登录后可调用接口。</span>
|
||
</form>
|
||
<script src="/static/client_auth.js" defer></script>
|
||
<section class="controls">
|
||
<form id="form">
|
||
<label for="file">人脸照片</label>
|
||
<input id="file" type="file" accept="image/jpeg,image/png" required>
|
||
<button id="submit" type="submit">生成标注</button>
|
||
<button id="sample" type="button">测试样例</button>
|
||
</form>
|
||
<p id="status" role="status">请选择 JPG 或 PNG 图片。</p>
|
||
</section>
|
||
<div id="results" class="results" hidden>
|
||
<section class="panel">
|
||
<h2>标注预览</h2>
|
||
<div class="photo">
|
||
<img id="base" alt="扶正裁切后的底图">
|
||
<img id="overlay" alt="白点、虚线及距离标注">
|
||
</div>
|
||
<p><a id="download" download="interface72-overlay.png">下载透明标注层</a></p>
|
||
</section>
|
||
<section class="panel">
|
||
<h2>测量结果</h2>
|
||
<table><thead><tr><th>点位</th><th>距离</th></tr></thead><tbody id="distances"></tbody></table>
|
||
<h2>坐标(像素)</h2>
|
||
<table><thead><tr><th>点号</th><th>X</th><th>Y</th></tr></thead><tbody id="points"></tbody></table>
|
||
<p class="note">点 6(MediaPipe 446)是射线锚点。六个白点的直径相同。</p>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
<script>
|
||
const byId = (id) => document.getElementById(id);
|
||
const status = byId("status");
|
||
const results = byId("results");
|
||
const submit = byId("submit");
|
||
const sample = byId("sample");
|
||
|
||
function setRows(id, rows) {
|
||
const body = byId(id);
|
||
body.replaceChildren();
|
||
for (const values of rows) {
|
||
const row = body.insertRow();
|
||
for (const value of values) row.insertCell().textContent = value;
|
||
}
|
||
}
|
||
|
||
function imageSource(data, field, mime) {
|
||
return data[field + "_url"] || ("data:" + mime + ";base64," + data[field + "_base64"]);
|
||
}
|
||
|
||
async function runAnnotation(loadFile) {
|
||
submit.disabled = sample.disabled = true;
|
||
results.hidden = true;
|
||
status.dataset.error = "false";
|
||
status.textContent = "正在扶正裁切并生成标注…";
|
||
try {
|
||
const file = await loadFile();
|
||
const form = new FormData();
|
||
form.append("image_file", file, file.name || "sample.jpg");
|
||
const response = await window.hairClientFetch("/api/v1/face/b-end-points", { method: "POST", body: form });
|
||
const { code, message, data } = await response.json();
|
||
if (!response.ok || code !== 0) throw new Error(message || "请求失败");
|
||
|
||
byId("base").src = imageSource(data, "input_image", "image/jpeg");
|
||
const overlay = imageSource(data, "annotated_image", "image/png");
|
||
byId("overlay").src = overlay;
|
||
byId("download").href = overlay;
|
||
setRows("distances", Object.entries(data.measurements_cm).map(([key, value]) => [
|
||
key, value.toFixed(1) + " cm"
|
||
]));
|
||
setRows("points", Object.entries(data.points).map(([key, point]) => [
|
||
key, point.x.toFixed(1), point.y.toFixed(1)
|
||
]));
|
||
results.hidden = false;
|
||
status.textContent = "标注完成。";
|
||
} catch (error) {
|
||
status.dataset.error = "true";
|
||
status.textContent = error.message;
|
||
} finally {
|
||
submit.disabled = sample.disabled = false;
|
||
}
|
||
}
|
||
|
||
byId("form").addEventListener("submit", (event) => {
|
||
event.preventDefault();
|
||
const file = byId("file").files[0];
|
||
if (file) runAnnotation(() => file);
|
||
});
|
||
|
||
sample.addEventListener("click", () => runAnnotation(async () => {
|
||
const response = await fetch("/static/sample.jpg");
|
||
if (!response.ok) throw new Error("无法读取样例图片");
|
||
return response.blob();
|
||
}));
|
||
</script>
|
||
</body>
|
||
</html>
|