tinymodel / index.html
icekun's picture
Update index.html
e0b7d1a verified
Raw History Blame
16 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>その画像、必要?</title>
<!-- ONNX Runtime Web -->
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
<style>
:root {
--orange-primary: #ff7c00;
--orange-hover: #e56f00;
--bg-main: #f9fafb;
--block-bg: #ffffff;
--border-color: #e5e7eb;
--text-title: #111827;
--text-body: #374151;
--text-muted: #6b7280;
--radius-block: 10px;
--radius-input: 6px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-main);
color: var(--text-body);
padding: 2rem 1rem;
display: flex;
justify-content: center;
}
.gradio-container {
width: 100%;
max-width: 960px;
background: var(--bg-main);
}
.gradio-header {
margin-bottom: 1.5rem;
text-align: left;
}
.gradio-header h1 {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-title);
margin-bottom: 0.5rem;
}
.gradio-header p {
color: var(--text-muted);
font-size: 0.95rem;
}
.gradio-layout {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 768px) {
.gradio-layout {
grid-template-columns: 1fr 1fr;
}
}
.gradio-column {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.gr-block {
background: var(--block-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-block);
padding: 1rem;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.gr-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 0.5rem;
display: block;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.gr-select {
width: 100%;
padding: 0.6rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-input);
background-color: #fff;
font-size: 0.95rem;
color: var(--text-title);
outline: none;
cursor: pointer;
}
.gr-select:focus {
border-color: var(--orange-primary);
}
/* Gradio Image Upload Component */
.gr-image-upload {
border: 2px dashed var(--border-color);
border-radius: var(--radius-input);
padding: 2rem 1rem;
text-align: center;
background-color: #f9fafb;
cursor: pointer;
position: relative;
transition: all 0.2s;
min-height: 220px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.gr-image-upload:hover {
border-color: var(--orange-primary);
background-color: #fff7ed;
}
.gr-image-upload input[type="file"] {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
.upload-icon {
font-size: 2.5rem;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.upload-text {
font-size: 0.9rem;
color: var(--text-muted);
}
.preview-img {
max-width: 100%;
max-height: 260px;
border-radius: var(--radius-input);
display: none;
object-fit: contain;
}
/* Gradio Button */
.gr-button-primary {
width: 100%;
padding: 0.75rem;
background-color: var(--orange-primary);
color: #ffffff;
border: none;
border-radius: var(--radius-input);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.15s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.gr-button-primary:hover:not(:disabled) {
background-color: var(--orange-hover);
}
.gr-button-primary:disabled {
background-color: #d1d5db;
cursor: not-allowed;
box-shadow: none;
}
/* Status Banner */
.status-banner {
font-size: 0.85rem;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-input);
background-color: #f3f4f6;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Gradio Label / Output Result Component */
.gr-label-output {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.top-class-box {
text-align: center;
padding: 1rem;
background-color: #fff7ed;
border: 1px solid #ffedd5;
border-radius: var(--radius-input);
margin-bottom: 0.5rem;
}
.top-class-name {
font-size: 1.5rem;
font-weight: 700;
color: var(--orange-primary);
}
.top-class-conf {
font-size: 0.9rem;
color: var(--text-muted);
}
.prob-row {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.prob-header {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
font-weight: 600;
}
.prob-bar-bg {
height: 20px;
background-color: #f3f4f6;
border-radius: 4px;
overflow: hidden;
}
.prob-bar-fill {
height: 100%;
background-color: var(--orange-primary);
width: 0%;
transition: width 0.4s ease;
}
.prob-bar-fill.secondary {
background-color: #9ca3af;
}
.empty-output {
text-align: center;
color: var(--text-muted);
padding: 3rem 1rem;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="gradio-container">
<div class="gradio-header">
<h1>必要な画像かどうかを判別</h1>
<p>画像をアップロードし、ドロップダウンで選択した .onnx モデルの判定結果(need / trash)を表示します。</p>
</div>
<div class="gradio-layout">
<!-- 左カラム:入力 -->
<div class="gradio-column">
<div class="gr-block">
<span class="gr-label">使用するモデル重み (.onnx)</span>
<select id="modelSelect" class="gr-select"></select>
</div>
<div class="gr-block">
<span class="gr-label">入力画像</span>
<div class="gr-image-upload" id="dropzone">
<input type="file" id="fileInput" accept="image/*">
<div id="uploadPrompt">
<div class="upload-icon">📁</div>
<div class="upload-text">クリックまたはドラッグ&ドロップで画像をアップロード</div>
</div>
<img id="previewImg" class="preview-img" alt="Preview">
</div>
</div>
<button id="submitBtn" class="gr-button-primary" disabled>Submit</button>
<div id="statusBanner" class="status-banner">
<span id="statusText">モデルをロード中...</span>
</div>
</div>
<!-- 右カラム:出力 -->
<div class="gradio-column">
<div class="gr-block" style="min-height: 100%;">
<span class="gr-label">判定結果 (確率)</span>
<div id="emptyOutput" class="empty-output">
画像を送信すると判定結果がここに表示されます。
</div>
<div id="resultOutput" class="gr-label-output" style="display: none;">
<div class="top-class-box">
<div id="topClassName" class="top-class-name">need</div>
<div id="topClassConf" class="top-class-conf">信頼度: 100%</div>
</div>
<div class="prob-row">
<div class="prob-header">
<span>need</span>
<span id="probNeedText">0%</span>
</div>
<div class="prob-bar-bg">
<div id="probNeedBar" class="prob-bar-fill"></div>
</div>
</div>
<div class="prob-row">
<div class="prob-header">
<span>trash</span>
<span id="probTrashText">0%</span>
</div>
<div class="prob-bar-bg">
<div id="probTrashBar" class="prob-bar-fill secondary"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const models = ["best_model.onnx", "model_epoch_1_acc_0.9849.onnx", "model_epoch_2_acc_0.9885.onnx", "model_epoch_3_acc_0.9865.onnx", "model_epoch_4_acc_0.9874.onnx", "model_epoch_5_acc_0.9877.onnx", "model_epoch_6_acc_0.9901.onnx", "model_epoch_7_acc_0.9908.onnx", "model_epoch_8_acc_0.9912.onnx", "model_epoch_9_acc_0.9909.onnx", "model_epoch_10_acc_0.9411.onnx", "model_epoch_11_acc_0.9929.onnx", "model_epoch_12_acc_0.9922.onnx", "model_epoch_13_acc_0.9918.onnx", "model_epoch_14_acc_0.9922.onnx", "model_epoch_15_acc_0.9925.onnx", "model_epoch_16_acc_0.9906.onnx", "model_epoch_17_acc_0.9908.onnx", "model_epoch_18_acc_0.9926.onnx", "model_epoch_19_acc_0.9916.onnx", "model_epoch_20_acc_0.9915.onnx"];
let session = null;
let isReady = false;
const modelSelect = document.getElementById('modelSelect');
const fileInput = document.getElementById('fileInput');
const previewImg = document.getElementById('previewImg');
const uploadPrompt = document.getElementById('uploadPrompt');
const submitBtn = document.getElementById('submitBtn');
const statusText = document.getElementById('statusText');
const emptyOutput = document.getElementById('emptyOutput');
const resultOutput = document.getElementById('resultOutput');
function initSelect() {
models.forEach(m => {
const opt = document.createElement('option');
opt.value = m;
opt.textContent = m;
modelSelect.appendChild(opt);
});
modelSelect.addEventListener('change', (e) => loadModel(e.target.value));
}
async function loadModel(modelName) {
isReady = false;
submitBtn.disabled = true;
statusText.innerText = "⏳ " + modelName + " をロード中...";
try {
if (session) {
await session.release();
session = null;
}
session = await ort.InferenceSession.create('./' + modelName);
isReady = true;
statusText.innerText = "✅ " + modelName + " の準備完了";
if (previewImg.src && previewImg.style.display !== 'none') {
submitBtn.disabled = false;
}
} catch (e) {
statusText.innerText = "❌ モデル読込エラー: " + e.message;
}
}
function handleFile(file) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (e) => {
previewImg.src = e.target.result;
previewImg.style.display = 'block';
uploadPrompt.style.display = 'none';
resultOutput.style.display = 'none';
emptyOutput.style.display = 'block';
if (isReady) submitBtn.disabled = false;
};
reader.readAsDataURL(file);
}
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
async function preprocess(imgElement) {
const canvas = document.createElement('canvas');
canvas.width = 224;
canvas.height = 224;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgElement, 0, 0, 224, 224);
const data = ctx.getImageData(0, 0, 224, 224).data;
const mean = [0.485, 0.456, 0.406];
const std = [0.229, 0.224, 0.225];
const floatData = new Float32Array(3 * 224 * 224);
for (let i = 0; i < 224 * 224; i++) {
const r = data[i * 4] / 255.0;
const g = data[i * 4 + 1] / 255.0;
const b = data[i * 4 + 2] / 255.0;
floatData[i] = (r - mean[0]) / std[0];
floatData[224 * 224 + i] = (g - mean[1]) / std[1];
floatData[2 * 224 * 224 + i] = (b - mean[2]) / std[2];
}
return new ort.Tensor('float32', floatData, [1, 3, 224, 224]);
}
submitBtn.addEventListener('click', async () => {
if (!isReady || !session || !previewImg.src) return;
submitBtn.disabled = true;
statusText.innerText = "⚡ 推論中...";
try {
const inputTensor = await preprocess(previewImg);
const feeds = {};
feeds[session.inputNames[0]] = inputTensor;
const start = performance.now();
const results = await session.run(feeds);
const duration = (performance.now() - start).toFixed(1);
const output = results[session.outputNames[0]].data;
const exp0 = Math.exp(output[0]);
const exp1 = Math.exp(output[1]);
const sum = exp0 + exp1;
const probNeed = (exp0 / sum) * 100;
const probTrash = (exp1 / sum) * 100;
// UI Render
emptyOutput.style.display = 'none';
resultOutput.style.display = 'flex';
const topClass = probNeed >= probTrash ? "need" : "trash";
const topConf = Math.max(probNeed, probTrash).toFixed(1);
document.getElementById('topClassName').innerText = topClass;
document.getElementById('topClassConf').innerText = "信頼度: " + topConf + "%";
document.getElementById('probNeedText').innerText = probNeed.toFixed(1) + "%";
document.getElementById('probNeedBar').style.width = probNeed + "%";
document.getElementById('probTrashText').innerText = probTrash.toFixed(1) + "%";
document.getElementById('probTrashBar').style.width = probTrash + "%";
statusText.innerText = "✅ 完了 (" + duration + "ms)";
} catch (e) {
statusText.innerText = "❌ エラー: " + e.message;
} finally {
submitBtn.disabled = false;
}
});
window.onload = () => {
initSelect();
if (models.length > 0) loadModel(models[0]);
};
</script>
</body>
</html>