icekun/tinymodel
0
1<!DOCTYPE html>2<html lang="ja">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>その画像、必要?</title>7 <!-- ONNX Runtime Web -->8 <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>9 <style>10 :root {11 --orange-primary: #ff7c00;12 --orange-hover: #e56f00;13 --bg-main: #f9fafb;14 --block-bg: #ffffff;15 --border-color: #e5e7eb;16 --text-title: #111827;17 --text-body: #374151;18 --text-muted: #6b7280;19 --radius-block: 10px;20 --radius-input: 6px;21 }22 23 * {24 box-sizing: border-box;25 margin: 0;26 padding: 0;27 }28 29 body {30 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;31 background-color: var(--bg-main);32 color: var(--text-body);33 padding: 2rem 1rem;34 display: flex;35 justify-content: center;36 }37 38 .gradio-container {39 width: 100%;40 max-width: 960px;41 background: var(--bg-main);42 }43 44 .gradio-header {45 margin-bottom: 1.5rem;46 text-align: left;47 }48 49 .gradio-header h1 {50 font-size: 1.75rem;51 font-weight: 700;52 color: var(--text-title);53 margin-bottom: 0.5rem;54 }55 56 .gradio-header p {57 color: var(--text-muted);58 font-size: 0.95rem;59 }60 61 .gradio-layout {62 display: grid;63 grid-template-columns: 1fr;64 gap: 1.5rem;65 }66 67 @media (min-width: 768px) {68 .gradio-layout {69 grid-template-columns: 1fr 1fr;70 }71 }72 73 .gradio-column {74 display: flex;75 flex-direction: column;76 gap: 1.25rem;77 }78 79 .gr-block {80 background: var(--block-bg);81 border: 1px solid var(--border-color);82 border-radius: var(--radius-block);83 padding: 1rem;84 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);85 }86 87 .gr-label {88 font-size: 0.85rem;89 font-weight: 600;90 color: var(--text-muted);91 margin-bottom: 0.5rem;92 display: block;93 text-transform: uppercase;94 letter-spacing: 0.05em;95 }96 97 .gr-select {98 width: 100%;99 padding: 0.6rem 0.75rem;100 border: 1px solid var(--border-color);101 border-radius: var(--radius-input);102 background-color: #fff;103 font-size: 0.95rem;104 color: var(--text-title);105 outline: none;106 cursor: pointer;107 }108 109 .gr-select:focus {110 border-color: var(--orange-primary);111 }112 113 /* Gradio Image Upload Component */114 .gr-image-upload {115 border: 2px dashed var(--border-color);116 border-radius: var(--radius-input);117 padding: 2rem 1rem;118 text-align: center;119 background-color: #f9fafb;120 cursor: pointer;121 position: relative;122 transition: all 0.2s;123 min-height: 220px;124 display: flex;125 flex-direction: column;126 align-items: center;127 justify-content: center;128 }129 130 .gr-image-upload:hover {131 border-color: var(--orange-primary);132 background-color: #fff7ed;133 }134 135 .gr-image-upload input[type="file"] {136 position: absolute;137 width: 100%;138 height: 100%;139 top: 0;140 left: 0;141 opacity: 0;142 cursor: pointer;143 }144 145 .upload-icon {146 font-size: 2.5rem;147 color: var(--text-muted);148 margin-bottom: 0.5rem;149 }150 151 .upload-text {152 font-size: 0.9rem;153 color: var(--text-muted);154 }155 156 .preview-img {157 max-width: 100%;158 max-height: 260px;159 border-radius: var(--radius-input);160 display: none;161 object-fit: contain;162 }163 164 /* Gradio Button */165 .gr-button-primary {166 width: 100%;167 padding: 0.75rem;168 background-color: var(--orange-primary);169 color: #ffffff;170 border: none;171 border-radius: var(--radius-input);172 font-size: 1rem;173 font-weight: 600;174 cursor: pointer;175 transition: background-color 0.15s;176 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);177 }178 179 .gr-button-primary:hover:not(:disabled) {180 background-color: var(--orange-hover);181 }182 183 .gr-button-primary:disabled {184 background-color: #d1d5db;185 cursor: not-allowed;186 box-shadow: none;187 }188 189 /* Status Banner */190 .status-banner {191 font-size: 0.85rem;192 padding: 0.5rem 0.75rem;193 border-radius: var(--radius-input);194 background-color: #f3f4f6;195 color: var(--text-muted);196 display: flex;197 align-items: center;198 gap: 0.5rem;199 }200 201 /* Gradio Label / Output Result Component */202 .gr-label-output {203 display: flex;204 flex-direction: column;205 gap: 0.75rem;206 }207 208 .top-class-box {209 text-align: center;210 padding: 1rem;211 background-color: #fff7ed;212 border: 1px solid #ffedd5;213 border-radius: var(--radius-input);214 margin-bottom: 0.5rem;215 }216 217 .top-class-name {218 font-size: 1.5rem;219 font-weight: 700;220 color: var(--orange-primary);221 }222 223 .top-class-conf {224 font-size: 0.9rem;225 color: var(--text-muted);226 }227 228 .prob-row {229 display: flex;230 flex-direction: column;231 gap: 0.25rem;232 }233 234 .prob-header {235 display: flex;236 justify-content: space-between;237 font-size: 0.85rem;238 font-weight: 600;239 }240 241 .prob-bar-bg {242 height: 20px;243 background-color: #f3f4f6;244 border-radius: 4px;245 overflow: hidden;246 }247 248 .prob-bar-fill {249 height: 100%;250 background-color: var(--orange-primary);251 width: 0%;252 transition: width 0.4s ease;253 }254 255 .prob-bar-fill.secondary {256 background-color: #9ca3af;257 }258 259 .empty-output {260 text-align: center;261 color: var(--text-muted);262 padding: 3rem 1rem;263 font-size: 0.9rem;264 }265 </style>266</head>267<body>268 269<div class="gradio-container">270 <div class="gradio-header">271 <h1>必要な画像かどうかを判別</h1>272 <p>画像をアップロードし、ドロップダウンで選択した .onnx モデルの判定結果(need / trash)を表示します。</p>273 </div>274 275 <div class="gradio-layout">276 <!-- 左カラム:入力 -->277 <div class="gradio-column">278 <div class="gr-block">279 <span class="gr-label">使用するモデル重み (.onnx)</span>280 <select id="modelSelect" class="gr-select"></select>281 </div>282 283 <div class="gr-block">284 <span class="gr-label">入力画像</span>285 <div class="gr-image-upload" id="dropzone">286 <input type="file" id="fileInput" accept="image/*">287 <div id="uploadPrompt">288 <div class="upload-icon">📁</div>289 <div class="upload-text">クリックまたはドラッグ&ドロップで画像をアップロード</div>290 </div>291 <img id="previewImg" class="preview-img" alt="Preview">292 </div>293 </div>294 295 <button id="submitBtn" class="gr-button-primary" disabled>Submit</button>296 297 <div id="statusBanner" class="status-banner">298 <span id="statusText">モデルをロード中...</span>299 </div>300 </div>301 302 <!-- 右カラム:出力 -->303 <div class="gradio-column">304 <div class="gr-block" style="min-height: 100%;">305 <span class="gr-label">判定結果 (確率)</span>306 307 <div id="emptyOutput" class="empty-output">308 画像を送信すると判定結果がここに表示されます。309 </div>310 311 <div id="resultOutput" class="gr-label-output" style="display: none;">312 <div class="top-class-box">313 <div id="topClassName" class="top-class-name">need</div>314 <div id="topClassConf" class="top-class-conf">信頼度: 100%</div>315 </div>316 317 <div class="prob-row">318 <div class="prob-header">319 <span>need</span>320 <span id="probNeedText">0%</span>321 </div>322 <div class="prob-bar-bg">323 <div id="probNeedBar" class="prob-bar-fill"></div>324 </div>325 </div>326 327 <div class="prob-row">328 <div class="prob-header">329 <span>trash</span>330 <span id="probTrashText">0%</span>331 </div>332 <div class="prob-bar-bg">333 <div id="probTrashBar" class="prob-bar-fill secondary"></div>334 </div>335 </div>336 </div>337 </div>338 </div>339 </div>340</div>341<script>342 // モデル定義(JSONの閾値 threshold: 0.19671 を追加)343 const models = [344 { file: "tinymodelV9s.onnx", type: "hsc", size: 256, threshold: 0.3 }, // 最適化閾値を適用345 { file: "tinymodelV3.onnx", type: "hsc", size: 224, threshold: 0.5 },346 { file: "tinymodelV2.onnx", type: "softmax", size: 224, threshold: 0.5 }347 ];348 349 let session = null;350 let isReady = false;351 let currentModelConfig = null;352 353 const modelSelect = document.getElementById('modelSelect');354 const fileInput = document.getElementById('fileInput');355 const previewImg = document.getElementById('previewImg');356 const uploadPrompt = document.getElementById('uploadPrompt');357 const submitBtn = document.getElementById('submitBtn');358 const statusText = document.getElementById('statusText');359 const emptyOutput = document.getElementById('emptyOutput');360 const resultOutput = document.getElementById('resultOutput');361 362 function initSelect() {363 modelSelect.innerHTML = '';364 models.forEach((m, index) => {365 const opt = document.createElement('option');366 opt.value = index;367 opt.textContent = m.file;368 modelSelect.appendChild(opt);369 });370 modelSelect.addEventListener('change', (e) => loadModel(models[e.target.value]));371 }372 373 async function loadModel(modelConfig) {374 isReady = false;375 submitBtn.disabled = true;376 currentModelConfig = modelConfig;377 statusText.innerText = "⏳ " + modelConfig.file + " をロード中...";378 379 try {380 if (session) {381 await session.release();382 session = null;383 }384 session = await ort.InferenceSession.create('./' + modelConfig.file);385 isReady = true;386 statusText.innerText = "✅ " + modelConfig.file + " の準備完了 (閾値: " + (modelConfig.threshold * 100).toFixed(1) + "%)";387 388 if (previewImg.src && previewImg.style.display !== 'none') {389 submitBtn.disabled = false;390 }391 } catch (e) {392 statusText.innerText = "❌ モデル読込エラー: " + e.message;393 }394 }395 396 function handleFile(file) {397 if (!file || !file.type.startsWith('image/')) return;398 const reader = new FileReader();399 reader.onload = (e) => {400 previewImg.src = e.target.result;401 previewImg.style.display = 'block';402 uploadPrompt.style.display = 'none';403 resultOutput.style.display = 'none';404 emptyOutput.style.display = 'block';405 if (isReady) submitBtn.disabled = false;406 };407 reader.readAsDataURL(file);408 }409 410 fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));411 412 async function preprocess(imgElement, targetSize) {413 const canvas = document.createElement('canvas');414 canvas.width = targetSize;415 canvas.height = targetSize;416 const ctx = canvas.getContext('2d');417 ctx.drawImage(imgElement, 0, 0, targetSize, targetSize);418 const data = ctx.getImageData(0, 0, targetSize, targetSize).data;419 420 const mean = [0.485, 0.456, 0.406];421 const std = [0.229, 0.224, 0.225];422 423 const floatData = new Float32Array(3 * targetSize * targetSize);424 for (let i = 0; i < targetSize * targetSize; i++) {425 const r = data[i * 4] / 255.0;426 const g = data[i * 4 + 1] / 255.0;427 const b = data[i * 4 + 2] / 255.0;428 floatData[i] = (r - mean[0]) / std[0];429 floatData[targetSize * targetSize + i] = (g - mean[1]) / std[1];430 floatData[2 * targetSize * targetSize + i] = (b - mean[2]) / std[2];431 }432 return new ort.Tensor('float32', floatData, [1, 3, targetSize, targetSize]);433 }434 435 submitBtn.addEventListener('click', async () => {436 if (!isReady || !session || !previewImg.src || !currentModelConfig) return;437 438 submitBtn.disabled = true;439 statusText.innerText = "⚡ 推論中...";440 try {441 const targetSize = currentModelConfig.size || 224;442 const origW = previewImg.naturalWidth;443 const origH = previewImg.naturalHeight;444 445 let probNeed, probTrash, rawTrashScore;446 447 // 256px 以下の低解像度画像の除外判定 (inf.py 仕様)448 if (currentModelConfig.file === "tinymodelV9s.onnx" && (origW <= 256 || origH <= 256)) {449 rawTrashScore = 1.0;450 probTrash = 100.0;451 probNeed = 0.0;452 statusText.innerText = "⚠️ 低解像度のため判定をスキップ (Trash)";453 } else {454 const inputTensor = await preprocess(previewImg, targetSize);455 const feeds = {};456 feeds[session.inputNames[0]] = inputTensor;457 458 const start = performance.now();459 const results = await session.run(feeds);460 const duration = (performance.now() - start).toFixed(1);461 462 const output = results[session.outputNames[0]].data;463 464 if (currentModelConfig.type === "hsc") {465 rawTrashScore = output[0];466 probTrash = rawTrashScore * 100;467 probNeed = (1.0 - rawTrashScore) * 100;468 } else if (currentModelConfig.type === "softmax") {469 const exp0 = Math.exp(output[0]);470 const exp1 = Math.exp(output[1]);471 const sum = exp0 + exp1;472 probNeed = (exp0 / sum) * 100;473 probTrash = (exp1 / sum) * 100;474 rawTrashScore = probTrash / 100.0;475 }476 statusText.innerText = "✅ 完了 (" + duration + "ms)";477 }478 479 // 判定閾値 (threshold) を使用した判定ロジック480 const threshold = currentModelConfig.threshold ?? 0.5;481 const topClass = rawTrashScore > threshold ? "trash" : "need";482 const topConf = (topClass === "trash" ? probTrash : probNeed).toFixed(1);483 484 // UI描画485 emptyOutput.style.display = 'none';486 resultOutput.style.display = 'flex';487 488 document.getElementById('topClassName').innerText = topClass;489 document.getElementById('topClassConf').innerText = "信頼度: " + topConf + "%";490 491 document.getElementById('probNeedText').innerText = probNeed.toFixed(1) + "%";492 document.getElementById('probNeedBar').style.width = probNeed + "%";493 document.getElementById('probTrashText').innerText = probTrash.toFixed(1) + "%";494 document.getElementById('probTrashBar').style.width = probTrash + "%";495 496 } catch (e) {497 statusText.innerText = "❌ エラー: " + e.message;498 } finally {499 submitBtn.disabled = false;500 }501 });502 503 window.onload = () => {504 initSelect();505 if (models.length > 0) loadModel(models[0]);506 };507</script>508 509</body>510</html>